From 93b5d122d3448b26bc87a449982c21a026f853f3 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Tue, 19 Mar 2024 10:48:23 +0100 Subject: [PATCH] feat! allow specifying default extent (#45) This improves performance in cases where default extent is same for all items. This is a breaking change because the signature of extent provider has changed. --- lib/src/element.dart | 2 +- lib/src/extent_list.dart | 59 +++++++++++++++++++++++++--------- lib/src/extent_manager.dart | 2 +- lib/src/fenwick_tree.dart | 16 ++++++++- lib/src/render_object.dart | 2 +- lib/src/super_sliver_list.dart | 9 ++++-- test/extent_list_test.dart | 17 +++++++++- test/fenwick_tree_test.dart | 41 +++++++++++++++++++++++ 8 files changed, 126 insertions(+), 22 deletions(-) diff --git a/lib/src/element.dart b/lib/src/element.dart index c69e856..7dc055b 100644 --- a/lib/src/element.dart +++ b/lib/src/element.dart @@ -33,7 +33,7 @@ class SuperSliverMultiBoxAdaptorElement extends SliverMultiBoxAdaptorElement } @override - double estimateExtentForItem(int index) { + double estimateExtentForItem(int? index) { return renderObject.estimateExtentForItem(index); } diff --git a/lib/src/extent_list.dart b/lib/src/extent_list.dart index 3ea3a2d..fc8d341 100644 --- a/lib/src/extent_list.dart +++ b/lib/src/extent_list.dart @@ -33,6 +33,20 @@ class ResizableFloat64List { _maybeTrim(); } + void resizeWithDefault(int newSize, double defaultValue) { + assert(newSize >= 0); + if (newSize == _length) { + return; + } + _ensureCapacity(newSize); + final list = _list; + for (var i = _length; i < newSize; ++i) { + list[i] = defaultValue; + } + _length = newSize; + _maybeTrim(); + } + void insert(int index, double element) { _ensureCapacity(_length + 1); if (index < _length) { @@ -214,24 +228,44 @@ class ExtentList { _fenwickTree = null; } - void resize(int newSize, double Function(int index) defaultExtent) { + void resize(int newSize, double Function(int? index) defaultExtent) { assert(_extents.length == _dirty.length); final prevSize = _extents.length; final prevExtents = _extents; if (newSize < prevSize) { - for (var i = newSize; i < prevSize; ++i) { - _totalExtent -= prevExtents[i]; - _dirtyCount -= _dirty[i] ? 1 : 0; + if (prevSize - newSize < newSize) { + for (var i = newSize; i < prevSize; ++i) { + _totalExtent -= prevExtents[i]; + _dirtyCount -= _dirty[i] ? 1 : 0; + } + } else { + // In this case it's less work to count the extent and dirty items + // from scratch. + _totalExtent = 0; + _dirtyCount = 0; + for (var i = 0; i < newSize; ++i) { + _totalExtent += _extents[i]; + _dirtyCount += _dirty[i] ? 1 : 0; + } } } double addedDefaultExtent = 0.0; - _extents.resize(newSize, (index) { - final extent = defaultExtent(index); - addedDefaultExtent += extent; - return extent; - }); + final sameExtent = defaultExtent(null); + if (sameExtent > 0) { + _extents.resizeWithDefault(newSize, sameExtent); + if (newSize > prevSize) { + addedDefaultExtent = (newSize - prevSize) * sameExtent; + } + } else { + _extents.resize(newSize, (index) { + final extent = defaultExtent(index); + addedDefaultExtent += extent; + return extent; + }); + } + _dirty.length = newSize; if (newSize > prevSize) { @@ -251,12 +285,7 @@ class ExtentList { } FenwickTree _getOrBuildFenwickTree() { - if (_fenwickTree == null) { - _fenwickTree = FenwickTree(size: _extents.length); - for (var i = 0; i < _extents.length; ++i) { - _fenwickTree!.update(i, _extents[i]); - } - } + _fenwickTree ??= FenwickTree.fromList(list: _extents._list); return _fenwickTree!; } diff --git a/lib/src/extent_manager.dart b/lib/src/extent_manager.dart index 743acd6..428fb33 100644 --- a/lib/src/extent_manager.dart +++ b/lib/src/extent_manager.dart @@ -8,7 +8,7 @@ abstract class ExtentManagerDelegate { const ExtentManagerDelegate(); void onMarkNeedsLayout(); - double estimateExtentForItem(int index); + double estimateExtentForItem(int? index); double getOffsetToReveal( int index, double alignment, { diff --git a/lib/src/fenwick_tree.dart b/lib/src/fenwick_tree.dart index ddaf68c..fac6029 100644 --- a/lib/src/fenwick_tree.dart +++ b/lib/src/fenwick_tree.dart @@ -2,7 +2,21 @@ import "dart:typed_data"; /// Zero indexed Fenwick Tree class FenwickTree { - FenwickTree({required this.size}) : _tree = Float64List(size + 1); + FenwickTree({ + required this.size, + }) : _tree = Float64List(size + 1); + + FenwickTree.fromList({ + required Float64List list, + }) : size = list.length, + _tree = Float64List(list.length + 1) { + for (int i = 1; i <= size; ++i) { + _tree[i] += list[i - 1]; + if (i + (i & -i) <= size) { + _tree[i + (i & -i)] += _tree[i]; + } + } + } final int size; final Float64List _tree; diff --git a/lib/src/render_object.dart b/lib/src/render_object.dart index efc6e42..5213f91 100644 --- a/lib/src/render_object.dart +++ b/lib/src/render_object.dart @@ -439,7 +439,7 @@ class RenderSuperSliverList extends RenderSliverMultiBoxAdaptor }); } - double estimateExtentForItem(int index) { + double estimateExtentForItem(int? index) { return estimateExtent(index, constraints.crossAxisExtent); } diff --git a/lib/src/super_sliver_list.dart b/lib/src/super_sliver_list.dart index cc46ae9..1ca9103 100644 --- a/lib/src/super_sliver_list.dart +++ b/lib/src/super_sliver_list.dart @@ -280,7 +280,7 @@ class ListController extends ChangeNotifier { } typedef ExtentEstimationProvider = double Function( - int index, + int? index, double crossAxisExtent, ); @@ -396,6 +396,11 @@ class SuperSliverList extends SliverMultiBoxAdaptorWidget { /// out, either through scrolling or [extentPrecalculationPolicy], the actual /// extents are calculated and the scroll offset is adjusted to account for /// the difference between estimated and actual extents. + /// + /// The item index argument is nullable. If all estimated items have same extent, + /// the implementation should return non-zero extent for the `null` index. This saves + /// calls to extent estimation provider for large lists. + /// If each item has different extent, return zero for the `null` index. final ExtentEstimationProvider? extentEstimation; /// Optional policy that can be used to asynchronously precalculate the extents @@ -484,6 +489,6 @@ class _TimeSuperSliverListLayoutBudget extends SuperSliverListLayoutBudget { final Duration budget; } -double _defaultEstimateExtent(int index, double crossAxisExtent) { +double _defaultEstimateExtent(int? index, double crossAxisExtent) { return 100.0; } diff --git a/test/extent_list_test.dart b/test/extent_list_test.dart index 0e496c6..5b96649 100644 --- a/test/extent_list_test.dart +++ b/test/extent_list_test.dart @@ -47,6 +47,13 @@ void main() { expect(list[0], equals(10)); expect(list[299], equals(10)); }); + test("resizeWithDefault", () { + final list = ResizableFloat64List(); + list.resizeWithDefault(300, 10); + expect(list.length, equals(300)); + expect(list[0], equals(10)); + expect(list[299], equals(10)); + }); }); group("ExtentList", () { test("empty", () { @@ -75,7 +82,7 @@ void main() { expect(extentList.hasDirtyItems, isFalse); expect(extentList.totalExtent, equals(100)); - extentList.resize(4, (_) => 150.0); + extentList.resize(4, (i) => i == null ? 0 : 150.0); expect(extentList.hasDirtyItems, isTrue); expect(extentList.totalExtent, equals(400)); @@ -83,6 +90,14 @@ void main() { extentList.setExtent(3, 80); expect(extentList.hasDirtyItems, isFalse); expect(extentList.totalExtent, equals(100 + 70 + 80)); + + extentList.resize(100, (index) => 50); + expect(extentList.totalExtent, equals(5050)); + expect(extentList.dirtyItemCount, 96); + + extentList.resize(10, (index) => 50); + expect(extentList.totalExtent, equals(550)); + expect(extentList.dirtyItemCount, 6); }); test("cleanRange", () { final extentList = ExtentList(); diff --git a/test/fenwick_tree_test.dart b/test/fenwick_tree_test.dart index bd111fa..f22b061 100644 --- a/test/fenwick_tree_test.dart +++ b/test/fenwick_tree_test.dart @@ -1,3 +1,5 @@ +import "dart:typed_data"; + import "package:super_sliver_list/src/fenwick_tree.dart"; import "package:test/test.dart"; @@ -37,6 +39,26 @@ void main() { expect(tree.query(6), 17); expect(tree.query(10), 31); }); + test("FenwickTree from list", () { + final list = Float64List(10); + list[0] = 2; + list[1] = 1; + list[2] = 3; + list[3] = 4; + list[4] = 5; + list[5] = 1; + list[6] = 2; + list[7] = 3; + list[8] = 4; + list[9] = 5; + final tree = FenwickTree.fromList(list: list); + expect(tree.query(0), 0); + expect(tree.query(3), 6); + expect(tree.query(5), 15); + expect(tree.query(6), 16); + expect(tree.query(9), 25); + expect(tree.query(10), 30); + }); test("large FenwickTree", () { const size = 10000000; final tree = FenwickTree(size: size); @@ -55,4 +77,23 @@ void main() { total += i == 0 ? 100 : i; } }); + test("large FenwickTree from list", () { + const size = 10000000; + final list = Float64List(size); + for (var i = 0; i < size; ++i) { + list[i] = i.toDouble(); + } + final tree = FenwickTree.fromList(list: list); + double total = 0; + for (var i = 0; i < size; ++i) { + expect(tree.query(i), total); + total += i; + } + tree.update(0, 100); + total = 0; + for (var i = 0; i < size; ++i) { + expect(tree.query(i), total); + total += i == 0 ? 100 : i; + } + }); }