Skip to content

Commit

Permalink
Extract logic to CellMetricsAggregator (#37777)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #37777

This extracts the state and logic VirtualizedList uses to query information related to cell metrics. We will need to modify this (and other places) when fixing up RTL support for horizontal FlatList.

Changelog: [Internal]

Reviewed By: javache

Differential Revision: D46427052

fbshipit-source-id: 0a23f6c726447de0f20c583b4d507003efd6a754
  • Loading branch information
NickGerleman authored and facebook-github-bot committed Jun 8, 2023
1 parent 96225ce commit 8f7f0bf
Show file tree
Hide file tree
Showing 10 changed files with 367 additions and 307 deletions.
30 changes: 11 additions & 19 deletions packages/virtualized-lists/Lists/FillRateHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

'use strict';

import type {CellMetricProps} from './VirtualizedListProps';
import type {CellMetricProps} from './ListMetricsAggregator';
import ListMetricsAggregator from './ListMetricsAggregator';

export type FillRateInfo = Info;

Expand All @@ -27,13 +28,6 @@ class Info {
sample_count: number = 0;
}

type CellMetrics = {
inLayout?: boolean,
length: number,
offset: number,
...
};

const DEBUG = false;

let _listeners: Array<(Info) => void> = [];
Expand All @@ -51,7 +45,7 @@ let _sampleRate = DEBUG ? 1 : null;
class FillRateHelper {
_anyBlankStartTime: ?number = null;
_enabled = false;
_getCellMetrics: (index: number, props: CellMetricProps) => ?CellMetrics;
_listMetrics: ListMetricsAggregator;
_info: Info = new Info();
_mostlyBlankStartTime: ?number = null;
_samplesStartTime: ?number = null;
Expand Down Expand Up @@ -79,10 +73,8 @@ class FillRateHelper {
_minSampleCount = minSampleCount;
}

constructor(
getCellMetrics: (index: number, props: CellMetricProps) => ?CellMetrics,
) {
this._getCellMetrics = getCellMetrics;
constructor(listMetrics: ListMetricsAggregator) {
this._listMetrics = listMetrics;
this._enabled = (_sampleRate || 0) > Math.random();
this._resetData();
}
Expand Down Expand Up @@ -186,12 +178,12 @@ class FillRateHelper {

let blankTop = 0;
let first = cellsAroundViewport.first;
let firstFrame = this._getCellMetrics(first, props);
let firstFrame = this._listMetrics.getCellMetrics(first, props);
while (
first <= cellsAroundViewport.last &&
(!firstFrame || !firstFrame.inLayout)
(!firstFrame || !firstFrame.isMounted)
) {
firstFrame = this._getCellMetrics(first, props);
firstFrame = this._listMetrics.getCellMetrics(first, props);
first++;
}
// Only count blankTop if we aren't rendering the first item, otherwise we will count the header
Expand All @@ -204,12 +196,12 @@ class FillRateHelper {
}
let blankBottom = 0;
let last = cellsAroundViewport.last;
let lastFrame = this._getCellMetrics(last, props);
let lastFrame = this._listMetrics.getCellMetrics(last, props);
while (
last >= cellsAroundViewport.first &&
(!lastFrame || !lastFrame.inLayout)
(!lastFrame || !lastFrame.isMounted)
) {
lastFrame = this._getCellMetrics(last, props);
lastFrame = this._listMetrics.getCellMetrics(last, props);
last--;
}
// Only count blankBottom if we aren't rendering the last item, otherwise we will count the
Expand Down
179 changes: 179 additions & 0 deletions packages/virtualized-lists/Lists/ListMetricsAggregator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import type {Props as VirtualizedListProps} from './VirtualizedListProps';
import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils';

import invariant from 'invariant';

export type CellMetrics = {
/**
* Index of the item in the list
*/
index: number,
/**
* Length of the cell along the scrolling axis
*/
length: number,
/**
* Offset to the cell along the scrolling axis
*/
offset: number,
/**
* Whether the cell is last known to be mounted
*/
isMounted: boolean,
};

/**
* Subset of VirtualizedList props needed to calculate cell metrics
*/
export type CellMetricProps = {
data: VirtualizedListProps['data'],
getItemCount: VirtualizedListProps['getItemCount'],
getItem: VirtualizedListProps['getItem'],
getItemLayout?: VirtualizedListProps['getItemLayout'],
keyExtractor?: VirtualizedListProps['keyExtractor'],
...
};

/**
* Provides an interface to query information about the metrics of a list and its cells.
*/
export default class ListMetricsAggregator {
_averageCellLength = 0;
_frames: {[string]: CellMetrics} = {};
_highestMeasuredCellIndex = 0;
_totalCellLength = 0;
_totalCellsMeasured = 0;

/**
* Notify the ListMetricsAggregator that a cell has been laid out.
*
* @returns whether the cell layout has changed since last notification
*/
notifyCellLayout(
cellKey: string,
index: number,
length: number,
offset: number,
): boolean {
const next: CellMetrics = {
offset,
length,
index,
isMounted: true,
};
const curr = this._frames[cellKey];
if (
!curr ||
next.offset !== curr.offset ||
next.length !== curr.length ||
index !== curr.index
) {
this._totalCellLength += next.length - (curr ? curr.length : 0);
this._totalCellsMeasured += curr ? 0 : 1;
this._averageCellLength =
this._totalCellLength / this._totalCellsMeasured;
this._frames[cellKey] = next;
this._highestMeasuredCellIndex = Math.max(
this._highestMeasuredCellIndex,
index,
);
return true;
} else {
this._frames[cellKey].isMounted = true;
return false;
}
}

/**
* Notify ListMetricsAggregator that a cell has been unmounted.
*/
notifyCellUnmounted(cellKey: string): void {
const curr = this._frames[cellKey];
if (curr) {
this._frames[cellKey] = {...curr, isMounted: false};
}
}

/**
* Return the average length of the cells which have been measured
*/
getAverageCellLength(): number {
return this._averageCellLength;
}

/**
* Return the highest measured cell index
*/
getHighestMeasuredCellIndex(): number {
return this._highestMeasuredCellIndex;
}

/**
* Returns the exact metrics of a cell if it has already been laid out,
* otherwise an estimate based on the average length of previously measured
* cells
*/
getCellMetricsApprox(index: number, props: CellMetricProps): CellMetrics {
const frame = this.getCellMetrics(index, props);
if (frame && frame.index === index) {
// check for invalid frames due to row re-ordering
return frame;
} else {
const {data, getItemCount} = props;
invariant(
index >= 0 && index < getItemCount(data),
'Tried to get frame for out of range index ' + index,
);
return {
length: this._averageCellLength,
offset: this._averageCellLength * index,
index,
isMounted: false,
};
}
}

/**
* Returns the exact metrics of a cell if it has already been laid out
*/
getCellMetrics(index: number, props: CellMetricProps): ?CellMetrics {
const {data, getItem, getItemCount, getItemLayout} = props;
invariant(
index >= 0 && index < getItemCount(data),
'Tried to get frame for out of range index ' + index,
);
const keyExtractor = props.keyExtractor ?? defaultKeyExtractor;
const frame = this._frames[keyExtractor(getItem(data, index), index)];
if (!frame || frame.index !== index) {
if (getItemLayout) {
const {length, offset} = getItemLayout(data, index);
return {index, length, offset, isMounted: true};
}
}
return frame;
}

/**
* Gets an approximate offset to an item at a given index. Supports
* fractional indices.
*/
getCellOffsetApprox(index: number, props: CellMetricProps): number {
if (Number.isInteger(index)) {
return this.getCellMetricsApprox(index, props).offset;
} else {
const frameMetrics = this.getCellMetricsApprox(Math.floor(index), props);
const remainder = index - Math.floor(index);
return frameMetrics.offset + remainder * frameMetrics.length;
}
}
}
27 changes: 7 additions & 20 deletions packages/virtualized-lists/Lists/ViewabilityHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

'use strict';

import type {CellMetricProps} from './VirtualizedListProps';
import type {CellMetricProps} from './ListMetricsAggregator';
import ListMetricsAggregator from './ListMetricsAggregator';

const invariant = require('invariant');

Expand Down Expand Up @@ -104,14 +105,7 @@ class ViewabilityHelper {
props: CellMetricProps,
scrollOffset: number,
viewportHeight: number,
getCellMetrics: (
index: number,
props: CellMetricProps,
) => ?{
length: number,
offset: number,
...
},
listMetrics: ListMetricsAggregator,
// Optional optimization to reduce the scan size
renderRange?: {
first: number,
Expand Down Expand Up @@ -146,7 +140,7 @@ class ViewabilityHelper {
return [];
}
for (let idx = first; idx <= last; idx++) {
const metrics = getCellMetrics(idx, props);
const metrics = listMetrics.getCellMetrics(idx, props);
if (!metrics) {
continue;
}
Expand Down Expand Up @@ -181,14 +175,7 @@ class ViewabilityHelper {
props: CellMetricProps,
scrollOffset: number,
viewportHeight: number,
getCellMetrics: (
index: number,
props: CellMetricProps,
) => ?{
length: number,
offset: number,
...
},
listMetrics: ListMetricsAggregator,
createViewToken: (
index: number,
isViewable: boolean,
Expand All @@ -210,7 +197,7 @@ class ViewabilityHelper {
if (
(this._config.waitForInteraction && !this._hasInteracted) ||
itemCount === 0 ||
!getCellMetrics(0, props)
!listMetrics.getCellMetrics(0, props)
) {
return;
}
Expand All @@ -220,7 +207,7 @@ class ViewabilityHelper {
props,
scrollOffset,
viewportHeight,
getCellMetrics,
listMetrics,
renderRange,
);
}
Expand Down
30 changes: 8 additions & 22 deletions packages/virtualized-lists/Lists/VirtualizeUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

'use strict';

import type {CellMetricProps} from './VirtualizedListProps';
import type {CellMetricProps} from './ListMetricsAggregator';
import ListMetricsAggregator from './ListMetricsAggregator';

/**
* Used to find the indices of the frames that overlap the given offsets. Useful for finding the
Expand All @@ -20,14 +21,7 @@ import type {CellMetricProps} from './VirtualizedListProps';
export function elementsThatOverlapOffsets(
offsets: Array<number>,
props: CellMetricProps,
getCellMetrics: (
index: number,
props: CellMetricProps,
) => {
length: number,
offset: number,
...
},
listMetrics: ListMetricsAggregator,
zoomScale: number = 1,
): Array<number> {
const itemCount = props.getItemCount(props.data);
Expand All @@ -38,9 +32,8 @@ export function elementsThatOverlapOffsets(
let right = itemCount - 1;

while (left <= right) {
// eslint-disable-next-line no-bitwise
const mid = left + ((right - left) >>> 1);
const frame = getCellMetrics(mid, props);
const mid = left + Math.floor((right - left) / 2);
const frame = listMetrics.getCellMetricsApprox(mid, props);
const scaledOffsetStart = frame.offset * zoomScale;
const scaledOffsetEnd = (frame.offset + frame.length) * zoomScale;

Expand Down Expand Up @@ -106,14 +99,7 @@ export function computeWindowedRenderLimits(
first: number,
last: number,
},
getCellMetricsApprox: (
index: number,
props: CellMetricProps,
) => {
length: number,
offset: number,
...
},
listMetrics: ListMetricsAggregator,
scrollMetrics: {
dt: number,
offset: number,
Expand Down Expand Up @@ -152,7 +138,7 @@ export function computeWindowedRenderLimits(
const overscanEnd = Math.max(0, visibleEnd + leadFactor * overscanLength);

const lastItemOffset =
getCellMetricsApprox(itemCount - 1, props).offset * zoomScale;
listMetrics.getCellMetricsApprox(itemCount - 1, props).offset * zoomScale;
if (lastItemOffset < overscanBegin) {
// Entire list is before our overscan window
return {
Expand All @@ -165,7 +151,7 @@ export function computeWindowedRenderLimits(
let [overscanFirst, first, last, overscanLast] = elementsThatOverlapOffsets(
[overscanBegin, visibleBegin, visibleEnd, overscanEnd],
props,
getCellMetricsApprox,
listMetrics,
zoomScale,
);
overscanFirst = overscanFirst == null ? 0 : overscanFirst;
Expand Down
Loading

0 comments on commit 8f7f0bf

Please sign in to comment.