From 3462ed8985e30fa777f777cc651410d75392f3bd Mon Sep 17 00:00:00 2001 From: "Manu Mtz.-Almeida" Date: Fri, 9 Feb 2018 21:53:30 +0100 Subject: [PATCH] feat(virtual-scroll): items update fast path --- .../src/components/virtual-scroll/readme.md | 6 ++ .../components/virtual-scroll/test/basic.html | 17 +++-- .../test/virtual-scroll-utils.spec.ts | 12 ++-- .../virtual-scroll/virtual-scroll-utils.tsx | 33 +++++++-- .../virtual-scroll/virtual-scroll.tsx | 67 +++++++++++++++++-- 5 files changed, 111 insertions(+), 24 deletions(-) diff --git a/packages/core/src/components/virtual-scroll/readme.md b/packages/core/src/components/virtual-scroll/readme.md index 9ebbb50415..fc1f8ad989 100644 --- a/packages/core/src/components/virtual-scroll/readme.md +++ b/packages/core/src/components/virtual-scroll/readme.md @@ -381,6 +381,12 @@ which is an expensive operation and should be avoided if possible. ## Methods +#### markDirty() + + +#### markDirtyTail() + + #### positionForItem() diff --git a/packages/core/src/components/virtual-scroll/test/basic.html b/packages/core/src/components/virtual-scroll/test/basic.html index 6e9a10358c..64f22bbb1e 100644 --- a/packages/core/src/components/virtual-scroll/test/basic.html +++ b/packages/core/src/components/virtual-scroll/test/basic.html @@ -33,17 +33,20 @@ diff --git a/packages/core/src/components/virtual-scroll/test/virtual-scroll-utils.spec.ts b/packages/core/src/components/virtual-scroll/test/virtual-scroll-utils.spec.ts index f9a53bca09..92a3ec90db 100644 --- a/packages/core/src/components/virtual-scroll/test/virtual-scroll-utils.spec.ts +++ b/packages/core/src/components/virtual-scroll/test/virtual-scroll-utils.spec.ts @@ -60,7 +60,7 @@ describe('getRange', () => { expect(bounds).toEqual({ offset: 0, - length: 4, + length: 5, }); }); @@ -148,7 +148,7 @@ describe('resizeBuffer', () => { describe('calcCells', () => { it('should calculate cells without headers and itemHeight', () => { const items = ['0', 2, 'hola', {data: 'hello'}]; - const cells = calcCells(items, null, null, null, 10, 20, 30); + const cells = calcCells(items, null, null, null, 10, 20, 30, 0, 0, items.length); expect(cells).toEqual([ { type: CellType.Item, @@ -197,7 +197,7 @@ describe('calcCells', () => { called++; return index * 20 + 20; }; - const cells = calcCells(items, itemHeight, null, null, 10, 20, 30); + const cells = calcCells(items, itemHeight, null, null, 10, 20, 30, 0, 0, items.length); expect(called).toEqual(3); expect(cells).toEqual([ @@ -253,7 +253,7 @@ describe('calcCells', () => { called++; return index * 20 + 20; }; - const cells = calcCells(items, itemHeight, headerFn, footerFn, 10, 20, 30); + const cells = calcCells(items, itemHeight, headerFn, footerFn, 10, 20, 30, 0, 0, items.length); expect(cells).toHaveLength(5); expect(called).toEqual(3); expect(headerCalled).toEqual(3); @@ -317,7 +317,7 @@ describe('calcHeightIndex', () => { const footerFn: HeaderFn = (_, index) => { return (index === 2) ? 'my footer' : null; }; - const cells = calcCells(items, null, headerFn, footerFn, 10, 20, 50); + const cells = calcCells(items, null, headerFn, footerFn, 10, 20, 50, 0, 0, items.length); const buf = resizeBuffer(null, cells.length); const totalHeight = calcHeightIndex(buf, cells, 0); expect(buf.length).toEqual(7); @@ -506,7 +506,7 @@ function mockVirtualScroll( headerFn: HeaderFn = null, footerFn: HeaderFn = null ) { - const cells = calcCells(items, itemHeight, headerFn, footerFn, 10, 10, 30); + const cells = calcCells(items, itemHeight, headerFn, footerFn, 10, 10, 30, 0, 0, items.length); const heightIndex = resizeBuffer(null, cells.length); calcHeightIndex(heightIndex, cells, 0); return { items, heightIndex, cells }; diff --git a/packages/core/src/components/virtual-scroll/virtual-scroll-utils.tsx b/packages/core/src/components/virtual-scroll/virtual-scroll-utils.tsx index 6c9e4d972c..d6c68b3773 100644 --- a/packages/core/src/components/virtual-scroll/virtual-scroll-utils.tsx +++ b/packages/core/src/components/virtual-scroll/virtual-scroll-utils.tsx @@ -148,7 +148,7 @@ export function doRender( node.visible = visible; } - // dynamic height inference + // dynamic height if (cell.reads > 0) { updateCellHeight(cell, child); cell.reads--; @@ -182,7 +182,8 @@ export function getRange(heightIndex: Uint32Array, viewport: Viewport, buffer: n break; } } - const end = Math.min(i + buffer, heightIndex.length - 1); + + const end = Math.min(i + buffer, heightIndex.length); const length = end - offset; return { offset, length }; } @@ -197,6 +198,23 @@ export function getShouldUpdate(dirtyIndex: number, currentRange: Range, range: } +export function findCellIndex(cells: Cell[], index: number): number { + if (index === 0) { + return 0; + } + return cells.findIndex(c => c.index === index); +} + +export function inplaceUpdate(dst: Cell[], src: Cell[], offset: number) { + if (offset === 0 && src.length >= dst.length) { + return src; + } + for (let i = 0; i < src.length; i++) { + dst[i + offset] = src[i]; + } + return dst; +} + export function calcCells( items: any[], @@ -206,12 +224,15 @@ export function calcCells( approxHeaderHeight: number, approxFooterHeight: number, - approxItemHeight: number + approxItemHeight: number, + + j: number, + offset: number, + len: number ): Cell[] { const cells = []; - let j = 0; - - for (let i = 0; i < items.length; i++) { + const end = len + offset; + for (let i = offset; i < end; i++) { const item = items[i]; if (headerFn) { const value = headerFn(item, i, items); diff --git a/packages/core/src/components/virtual-scroll/virtual-scroll.tsx b/packages/core/src/components/virtual-scroll/virtual-scroll.tsx index b011cd901e..2d61ce4aa9 100644 --- a/packages/core/src/components/virtual-scroll/virtual-scroll.tsx +++ b/packages/core/src/components/virtual-scroll/virtual-scroll.tsx @@ -2,7 +2,7 @@ import { Component, Element, EventListenerEnable, Listen, Method, Prop, Watch } import { DomController } from '../../index'; import { Cell, DomRenderFn, HeaderFn, ItemHeightFn, ItemRenderFn, NodeHeightFn, Range, Viewport, VirtualNode, calcCells, calcHeightIndex, doRender, getRange, - getShouldUpdate, getViewport, positionForIndex, resizeBuffer, updateVDom } from './virtual-scroll-utils'; + getShouldUpdate, getViewport, positionForIndex, resizeBuffer, updateVDom, findCellIndex, inplaceUpdate } from './virtual-scroll-utils'; @Component({ @@ -24,6 +24,7 @@ export class VirtualScroll { private indexDirty = 0; private totalHeight = 0; private heightChanged = false; + private lastItemLen = 0; @Element() el: HTMLElement; @@ -144,6 +145,52 @@ export class VirtualScroll { return positionForIndex(index, this.cells, this.heightIndex); } + @Method() + markDirty(offset: number, len = -1) { + // TODO: kind of hacky how we do in-place updated of the cells + // array. this part needs a complete refactor + if (!this.items) { + return; + } + if (len === -1) { + len = this.items.length - offset; + } + const max = this.lastItemLen; + let j = 0; + if (offset > 0 && offset < max) { + j = findCellIndex(this.cells, offset); + } else if (offset === 0) { + j = 0; + } else if (offset === max) { + j = this.cells.length; + } else { + console.warn('bad values for markDirty'); + return; + } + const cells = calcCells( + this.items, + this.itemHeight, + this.headerFn, + this.footerFn, + this.approxHeaderHeight, + this.approxFooterHeight, + this.approxItemHeight, + j, offset, len + ); + console.debug('[virtual] cells recalculated', cells.length); + this.cells = inplaceUpdate(this.cells, cells, offset); + this.lastItemLen = this.items.length; + this.indexDirty = Math.max(offset - 1, 0); + + this.scheduleUpdate(); + } + + @Method() + markDirtyTail() { + const offset = this.lastItemLen; + this.markDirty(offset, this.items.length - offset); + } + private updateVirtualScroll() { // do nothing if there is a scheduled update if (!this.isEnabled || !this.scrollEl) { @@ -229,14 +276,18 @@ export class VirtualScroll { } cell.visible = true; if (cell.height !== height) { - console.debug(`[${cell.reads}] cell size ${cell.height} -> ${height}`); + console.debug(`[virtual] cell height changed ${cell.height}px -> ${height}px`); cell.height = height; - clearTimeout(this.timerUpdate); - this.timerUpdate = setTimeout(() => this.updateVirtualScroll(), 100); this.indexDirty = Math.min(this.indexDirty, index); + this.scheduleUpdate(); } } + private scheduleUpdate() { + clearTimeout(this.timerUpdate); + this.timerUpdate = setTimeout(() => this.updateVirtualScroll(), 100); + } + private updateState() { const shouldEnable = !!( this.scrollEl && @@ -252,10 +303,12 @@ export class VirtualScroll { } } + private calcCells() { if (!this.items) { return; } + this.lastItemLen = this.items.length; this.cells = calcCells( this.items, this.itemHeight, @@ -263,8 +316,10 @@ export class VirtualScroll { this.footerFn, this.approxHeaderHeight, this.approxFooterHeight, - this.approxItemHeight + this.approxItemHeight, + 0, 0, this.lastItemLen ); + console.debug('[virtual] cells recalculated', this.cells.length); this.indexDirty = 0; } @@ -279,9 +334,11 @@ export class VirtualScroll { this.heightIndex = resizeBuffer(this.heightIndex, this.cells.length); const totalHeight = calcHeightIndex(this.heightIndex, this.cells, index); if (totalHeight !== this.totalHeight) { + console.debug(`[virtual] total height changed: ${this.totalHeight}px -> ${totalHeight}px`); this.totalHeight = totalHeight; this.heightChanged = true; } + console.debug('[virtual] height index recalculated', this.heightIndex.length - index); this.indexDirty = Infinity; }