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

Masonry: Enable dynamic batches #3875

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
72 changes: 48 additions & 24 deletions packages/gestalt/src/Masonry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ import styles from './Masonry.css';
import { Cache } from './Masonry/Cache';
import recalcHeights from './Masonry/dynamicHeightsUtils';
import recalcHeightsV2 from './Masonry/dynamicHeightsV2Utils';
import getColumnCount, {
DEFAULT_LAYOUT_DEFAULT_GUTTER,
FULL_WIDTH_DEFAULT_GUTTER,
} from './Masonry/getColumnCount';
import getLayoutAlgorithm from './Masonry/getLayoutAlgorithm';
import ItemResizeObserverWrapper from './Masonry/ItemResizeObserverWrapper';
import MeasurementStore from './Masonry/MeasurementStore';
import { ColumnSpanConfig, MULTI_COL_ITEMS_MEASURE_BATCH_SIZE } from './Masonry/multiColumnLayout';
import {
calculateActualColumnSpan,
ColumnSpanConfig,
ModulePositioningConfig,
MULTI_COL_ITEMS_MEASURE_BATCH_SIZE,
} from './Masonry/multiColumnLayout';
import ScrollContainer from './Masonry/ScrollContainer';
import { getElementHeight, getRelativeScrollTop, getScrollPos } from './Masonry/scrollUtils';
import { Align, Layout, LoadingStateItem, Position } from './Masonry/types';
Expand Down Expand Up @@ -153,16 +162,17 @@ type Props<T> = {
*/
_dynamicHeightsV2Experiment?: boolean;
/**
/**
*
* Experimental prop to enable early bailout when positioning multicolumn modules
* Experimental prop to enable dynamic batch sizing and early bailout when positioning a module
* - Early bailout: How much whitespace is "good enough"
* - Dynamic batch sizing: How many items it can use. If this prop isn't used, it uses 5
*
* This is an experimental prop and may be removed or changed in the future
*/
_earlyBailout?: (columnSpan: number) => number;
_getModulePositioningConfig?: (gridSize: number, moduleSize: number) => ModulePositioningConfig;
};

type State<T> = {
gutter: number;
hasPendingMeasurements: boolean;
isFetching: boolean;
items: ReadonlyArray<T>;
Expand Down Expand Up @@ -220,6 +230,13 @@ export default class Masonry<T> extends ReactComponent<Props<T>, State<T>> {

this.positionStore = props.positionStore || Masonry.createMeasurementStore();

const { layout, gutterWidth } = props;
let defaultGutter = DEFAULT_LAYOUT_DEFAULT_GUTTER;
if ((layout && layout === 'flexible') || layout === 'serverRenderedFlexible') {
defaultGutter = FULL_WIDTH_DEFAULT_GUTTER;
}
const gutter = gutterWidth ?? defaultGutter;

this.resizeObserver =
/* eslint-disable-next-line no-underscore-dangle */
props._dynamicHeights && typeof window !== 'undefined' && this.positionStore
Expand All @@ -232,13 +249,6 @@ export default class Masonry<T> extends ReactComponent<Props<T>, State<T>> {
const changedItem: T = this.state.items[idx]!;
const newHeight = contentRect.height;

// TODO: DefaultGutter comes from getLayoutAlgorithm and their utils, everything should be in one place (this.gutter?)
const { layout, gutterWidth } = this.props;
let defaultGutter = 14;
if ((layout && layout === 'flexible') || layout === 'serverRenderedFlexible') {
defaultGutter = 0;
}

/* eslint-disable-next-line no-underscore-dangle */
if (props._dynamicHeightsV2Experiment) {
triggerUpdate =
Expand All @@ -248,7 +258,7 @@ export default class Masonry<T> extends ReactComponent<Props<T>, State<T>> {
newHeight,
positionStore: this.positionStore,
measurementStore: this.state.measurementStore,
gutterWidth: gutterWidth ?? defaultGutter,
gutter,
}) || triggerUpdate;
} else {
triggerUpdate =
Expand All @@ -269,6 +279,7 @@ export default class Masonry<T> extends ReactComponent<Props<T>, State<T>> {
: undefined;

this.state = {
gutter,
hasPendingMeasurements: props.items.some((item) => !!item && !measurementStore.has(item)),
isFetching: false,
items: props.items,
Expand Down Expand Up @@ -606,7 +617,6 @@ export default class Masonry<T> extends ReactComponent<Props<T>, State<T>> {
const {
align = 'center',
columnWidth,
gutterWidth: gutter,
items,
layout = 'basic',
minCols,
Expand All @@ -616,9 +626,9 @@ export default class Masonry<T> extends ReactComponent<Props<T>, State<T>> {
_getColumnSpanConfig,
_loadingStateItems = [],
_renderLoadingStateItems,
_earlyBailout,
_getModulePositioningConfig,
} = this.props;
const { hasPendingMeasurements, measurementStore, width } = this.state;
const { gutter, hasPendingMeasurements, measurementStore, width } = this.state;
const { positionStore } = this;
const renderLoadingState = Boolean(
items.length === 0 && _loadingStateItems && _renderLoadingStateItems,
Expand All @@ -638,7 +648,7 @@ export default class Masonry<T> extends ReactComponent<Props<T>, State<T>> {
_logTwoColWhitespace,
_loadingStateItems,
renderLoadingState,
_earlyBailout,
_getModulePositioningConfig,
});

let gridBody;
Expand Down Expand Up @@ -728,15 +738,29 @@ export default class Masonry<T> extends ReactComponent<Props<T>, State<T>> {
// Full layout is possible
const itemsToRender = items.filter((item) => item && measurementStore.has(item));
const itemsWithoutPositions = items.filter((item) => item && !positionStore.has(item));
const hasMultiColumnItems =
const nextMultiColumnItem =
_getColumnSpanConfig &&
itemsWithoutPositions.some((item) => _getColumnSpanConfig(item) !== 1);
itemsWithoutPositions.find((item) => _getColumnSpanConfig(item) !== 1);

let batchSize;
if (nextMultiColumnItem) {
const gridSize = getColumnCount({ gutter, columnWidth, width, minCols, layout });

const moduleSize = calculateActualColumnSpan({
Comment on lines +747 to +749
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the tricky part of this implementation. These two values are easy to find in multiColumnLayout.ts, but are not ready at this scope.

  • For grid size, I created a whole new util that abstract the way we calculate the column count from both DefaultLayout and FullWidthLayout (updating those utils to avoid code duplicity too).
  • For moduleSize, I exposed the calculateActualColumnSpan from multiColumnLayout.ts to be able to use it here.

All of this happens also in MasonryV2.

columnCount: gridSize,
item: nextMultiColumnItem,
_getColumnSpanConfig,
});

const { itemsBatchSize } = _getModulePositioningConfig?.(gridSize, moduleSize) || {
itemsBatchSize: MULTI_COL_ITEMS_MEASURE_BATCH_SIZE,
};
batchSize = itemsBatchSize;
}

// If there are 2-col items, we need to measure more items to ensure we have enough possible layouts to find a suitable one
// we need the batch size (number of one column items for the graph) + 1 (two column item)
const itemsToMeasureCount = hasMultiColumnItems
? MULTI_COL_ITEMS_MEASURE_BATCH_SIZE + 1
: minCols;
// If there are multicolumn items, we need to measure more items to ensure we have enough possible layouts to find a suitable one
// we need the batch size (number of one column items for the graph) + 1 (multicolumn item)
const itemsToMeasureCount = batchSize ? batchSize + 1 : minCols;
const itemsToMeasure = items
.filter((item) => item && !measurementStore.has(item))
.slice(0, itemsToMeasureCount);
Expand Down
10 changes: 10 additions & 0 deletions packages/gestalt/src/Masonry/defaultLayout.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import defaultLayout from './defaultLayout';
import { DEFAULT_LAYOUT_DEFAULT_GUTTER } from './getColumnCount';
import MeasurementStore from './MeasurementStore';
import { Position } from './types';

Expand Down Expand Up @@ -27,6 +28,7 @@ describe.each([undefined, getColumnSpanConfig])('default layout tests', (_getCol

const layout = defaultLayout({
align: 'start',
gutter: DEFAULT_LAYOUT_DEFAULT_GUTTER,
measurementCache: measurementStore,
positionCache,
layout: 'basic',
Expand Down Expand Up @@ -58,6 +60,7 @@ describe.each([undefined, getColumnSpanConfig])('default layout tests', (_getCol

const layout = defaultLayout({
align: 'center',
gutter: DEFAULT_LAYOUT_DEFAULT_GUTTER,
measurementCache: measurementStore,
positionCache,
layout: 'basic',
Expand Down Expand Up @@ -90,6 +93,7 @@ describe.each([undefined, getColumnSpanConfig])('default layout tests', (_getCol

const layout = defaultLayout({
align: 'center',
gutter: DEFAULT_LAYOUT_DEFAULT_GUTTER,
measurementCache: measurementStore,
positionCache,
layout: 'basicCentered',
Expand Down Expand Up @@ -122,6 +126,7 @@ describe.each([undefined, getColumnSpanConfig])('default layout tests', (_getCol

const layout = defaultLayout({
align: 'end',
gutter: DEFAULT_LAYOUT_DEFAULT_GUTTER,
measurementCache: measurementStore,
positionCache,
layout: 'basic',
Expand Down Expand Up @@ -154,6 +159,7 @@ describe.each([undefined, getColumnSpanConfig])('default layout tests', (_getCol

const layout = defaultLayout({
align: 'start',
gutter: DEFAULT_LAYOUT_DEFAULT_GUTTER,
measurementCache: measurementStore,
positionCache,
layout: 'basic',
Expand Down Expand Up @@ -185,6 +191,7 @@ describe.each([undefined, getColumnSpanConfig])('default layout tests', (_getCol

const layout = defaultLayout({
align: 'start',
gutter: DEFAULT_LAYOUT_DEFAULT_GUTTER,
measurementCache: measurementStore,
positionCache,
layout: 'basic',
Expand Down Expand Up @@ -266,6 +273,7 @@ describe.each([undefined, getColumnSpanConfig])('default layout tests', (_getCol

const layout = defaultLayout({
align: 'end',
gutter: DEFAULT_LAYOUT_DEFAULT_GUTTER,
measurementCache: measurementStore,
positionCache,
layout: 'basic',
Expand Down Expand Up @@ -299,6 +307,7 @@ describe('loadingStateItems', () => {

const layout = defaultLayout({
align: 'start',
gutter: DEFAULT_LAYOUT_DEFAULT_GUTTER,
measurementCache: measurementStore,
positionCache,
layout: 'basic',
Expand Down Expand Up @@ -335,6 +344,7 @@ describe('loadingStateItems', () => {

const layout = defaultLayout({
align: 'start',
gutter: DEFAULT_LAYOUT_DEFAULT_GUTTER,
measurementCache: measurementStore,
positionCache,
layout: 'basic',
Expand Down
21 changes: 15 additions & 6 deletions packages/gestalt/src/Masonry/defaultLayout.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Cache } from './Cache';
import getColumnCount, { DEFAULT_LAYOUT_DEFAULT_COLUMN_WIDTH } from './getColumnCount';
import { getHeightAndGutter, offscreen } from './layoutHelpers';
import { isLoadingStateItem, isLoadingStateItems } from './loadingStateUtils';
import mindex from './mindex';
import multiColumnLayout, { ColumnSpanConfig } from './multiColumnLayout';
import multiColumnLayout, { ColumnSpanConfig, ModulePositioningConfig } from './multiColumnLayout';
import { Align, Layout, LoadingStateItem, Position } from './types';

const calculateCenterOffset = ({
Expand Down Expand Up @@ -38,19 +39,20 @@ const calculateCenterOffset = ({
const defaultLayout =
<T>({
align,
columnWidth = 236,
gutter = 14,
columnWidth = DEFAULT_LAYOUT_DEFAULT_COLUMN_WIDTH,
gutter,
layout,
minCols = 2,
rawItemCount,
width,
measurementCache,
_getColumnSpanConfig,
_getModulePositioningConfig,
renderLoadingState,
...otherProps
}: {
columnWidth?: number;
gutter?: number;
gutter: number;
align: Align;
layout: Layout;
minCols?: number;
Expand All @@ -59,7 +61,7 @@ const defaultLayout =
positionCache: Cache<T, Position>;
measurementCache: Cache<T, number>;
_getColumnSpanConfig?: (item: T) => ColumnSpanConfig;
earlyBailout?: (columnSpan: number) => number;
_getModulePositioningConfig?: (gridSize: number, moduleSize: number) => ModulePositioningConfig;
logWhitespace?: (
additionalWhitespace: ReadonlyArray<number>,
numberOfIterations: number,
Expand All @@ -73,7 +75,13 @@ const defaultLayout =
}

const columnWidthAndGutter = columnWidth + gutter;
const columnCount = Math.max(Math.floor((width + gutter) / columnWidthAndGutter), minCols);
const columnCount = getColumnCount({
gutter,
columnWidth,
width,
minCols,
layout,
});
// the total height of each column
const heights = new Array<number>(columnCount).fill(0);

Expand All @@ -96,6 +104,7 @@ const defaultLayout =
gutter,
measurementCache,
_getColumnSpanConfig,
_getModulePositioningConfig,
...otherProps,
})
: items.map((item) => {
Expand Down
12 changes: 6 additions & 6 deletions packages/gestalt/src/Masonry/dynamicHeightsUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('dynamic heights on masonry', () => {
newHeight: items[changedItemIndex].height + heightDelta,
positionStore: positionCache,
measurementStore,
gutterWidth: gutter,
gutter,
});

items.forEach((item, index) => {
Expand Down Expand Up @@ -114,7 +114,7 @@ describe('dynamic heights on masonry', () => {
newHeight: items[changedItemIndex].height - heightDelta,
positionStore: positionCache,
measurementStore,
gutterWidth: gutter,
gutter,
});

items.forEach((item, index) => {
Expand Down Expand Up @@ -168,7 +168,7 @@ describe('dynamic heights on masonry', () => {
newHeight: items[changedItemIndex].height - heightDelta,
positionStore: positionCache,
measurementStore,
gutterWidth: gutter,
gutter,
});

items.forEach((item, index) => {
Expand Down Expand Up @@ -223,7 +223,7 @@ describe('dynamic heights on masonry', () => {
newHeight: firstItemNewHeight,
positionStore: positionCache,
measurementStore,
gutterWidth: gutter,
gutter,
});

const changedItemIndex2 = 1;
Expand All @@ -234,7 +234,7 @@ describe('dynamic heights on masonry', () => {
newHeight: items[changedItemIndex2].height + heightDelta,
positionStore: positionCache,
measurementStore,
gutterWidth: gutter,
gutter,
});

const twoColItemIndex = 2;
Expand Down Expand Up @@ -326,7 +326,7 @@ describe('dynamic heights on masonry', () => {
newHeight: changedItemIndexNewHeight,
positionStore: positionCache,
measurementStore,
gutterWidth: gutter,
gutter,
});

const expectedPos = [
Expand Down
Loading
Loading