Skip to content

Commit 7a254bd

Browse files
committed
Implements experimental diff with optimized moves calculation to produce only minimal required moves
1 parent f469c34 commit 7a254bd

File tree

5 files changed

+349
-2
lines changed

5 files changed

+349
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag
77

88
### Enhancements
99

10+
- Implemented experimental diff with optimized moves calculation to produce only minimal required moves, enabled by `IGListExperimentOptimizedMoves` option. [Nickolay Tarbayev](https://github.com/tarbayev) [(#1139)](https://github.com/Instagram/IGListKit/pull/1139)
11+
1012
- Add support for UICollectionView's interactive reordering in iOS 9+. Updates include `-[IGListSectionController canMoveItemAtIndex:]` to enable the behavior, `-[IGListSectionController moveObjectFromIndex:toIndex:]` called when items within a section controller were moved through reordering, `-[IGListAdapterDataSource listAdapter:moveObject:from:to]` called when section controllers themselves were reordered (only possible when all section controllers contain exactly 1 object), and `-[IGListUpdatingDelegate moveSectionInCollectionView:fromIndex:toIndex]` to enable custom updaters to conform to the reordering behavior. The update also includes two new examples `ReorderableSectionController` and `ReorderableStackedViewController` to demonstrate how to enable interactive reordering in your client app. [Jared Verdi](https://github.com/jverdi) [(#976)](https://github.com/Instagram/IGListKit/pull/976)
1113

1214
- 5x improvement to diffing performance when result is only inserts or deletes. [Ryan Nystrom](https://github.com/rnystrom) [(tbd)](tbd)

IGListKit.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
0179E635207FA8EB0082DFE6 /* IGListDiffExperimentOptimizedMovesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0179E634207FA8EB0082DFE6 /* IGListDiffExperimentOptimizedMovesTests.m */; };
11+
0179E636207FA8F30082DFE6 /* IGListDiffExperimentOptimizedMovesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0179E634207FA8EB0082DFE6 /* IGListDiffExperimentOptimizedMovesTests.m */; };
12+
0179E637207FA8F30082DFE6 /* IGListDiffExperimentOptimizedMovesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0179E634207FA8EB0082DFE6 /* IGListDiffExperimentOptimizedMovesTests.m */; };
1013
0B3B92DA1E08D7F5008390ED /* IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B3B928B1E08D7F5008390ED /* IGListKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
1114
0B3B92DB1E08D7F5008390ED /* IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B3B928B1E08D7F5008390ED /* IGListKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
1215
0B3B92F61E08D7F5008390ED /* IGListAdapter.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B3B929A1E08D7F5008390ED /* IGListAdapter.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -407,6 +410,7 @@
407410
/* End PBXContainerItemProxy section */
408411

409412
/* Begin PBXFileReference section */
413+
0179E634207FA8EB0082DFE6 /* IGListDiffExperimentOptimizedMovesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListDiffExperimentOptimizedMovesTests.m; sourceTree = "<group>"; };
410414
08F0B0FD0690F4FC46DDF21B /* Pods-IGListKit-tvOSTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-IGListKit-tvOSTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-IGListKit-tvOSTests/Pods-IGListKit-tvOSTests.release.xcconfig"; sourceTree = "<group>"; };
411415
0B3B928B1E08D7F5008390ED /* IGListKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListKit.h; sourceTree = "<group>"; };
412416
0B3B929A1E08D7F5008390ED /* IGListAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapter.h; sourceTree = "<group>"; };
@@ -975,6 +979,7 @@
975979
294AC6311DDE4C19002FCE5D /* IGListDiffResultTests.m */,
976980
88144EE61D870EDC007C7F66 /* IGListDiffSwiftTests.swift */,
977981
88144EE81D870EDC007C7F66 /* IGListDiffTests.m */,
982+
0179E634207FA8EB0082DFE6 /* IGListDiffExperimentOptimizedMovesTests.m */,
978983
88144EE91D870EDC007C7F66 /* IGListDisplayHandlerTests.m */,
979984
29DA5CA21EA7C72400113926 /* IGListGenericSectionControllerTests.m */,
980985
88144EEB1D870EDC007C7F66 /* IGListKitTests-Bridging-Header.h */,
@@ -1559,6 +1564,7 @@
15591564
files = (
15601565
298DDA381E3B168E00F76F50 /* IGLayoutTestItem.m in Sources */,
15611566
885FE2361DC51B76009CE2B4 /* IGListStackSectionControllerTests.m in Sources */,
1567+
0179E636207FA8F30082DFE6 /* IGListDiffExperimentOptimizedMovesTests.m in Sources */,
15621568
885FE2311DC51B76009CE2B4 /* IGListDisplayHandlerTests.m in Sources */,
15631569
298DDA3B1E3B16F800F76F50 /* IGLayoutTestDataSource.m in Sources */,
15641570
29C474901DDF460500AE68CE /* IGListSectionMapTests.m in Sources */,
@@ -1681,6 +1687,7 @@
16811687
294AC6321DDE4C19002FCE5D /* IGListDiffResultTests.m in Sources */,
16821688
88144F141D870EDC007C7F66 /* IGListTestOffsettingLayout.m in Sources */,
16831689
8240C7FB1DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.m in Sources */,
1690+
0179E635207FA8EB0082DFE6 /* IGListDiffExperimentOptimizedMovesTests.m in Sources */,
16841691
298DDA011E3AE28000F76F50 /* IGTestDiffingObject.m in Sources */,
16851692
88144F131D870EDC007C7F66 /* IGListTestAdapterDataSource.m in Sources */,
16861693
88144F071D870EDC007C7F66 /* IGListAdapterE2ETests.m in Sources */,
@@ -1732,6 +1739,7 @@
17321739
isa = PBXSourcesBuildPhase;
17331740
buildActionMask = 2147483647;
17341741
files = (
1742+
0179E637207FA8F30082DFE6 /* IGListDiffExperimentOptimizedMovesTests.m in Sources */,
17351743
88DF898A1E010F7000B1B9B4 /* IGListDiffTests.m in Sources */,
17361744
88DF89891E010F6500B1B9B4 /* IGListDiffSwiftTests.swift in Sources */,
17371745
882BC1321E0118CB0083B311 /* IGTestObject.m in Sources */,

Source/Common/IGListDiff.mm

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,123 @@ static void addIndexToCollection(BOOL useIndexPaths, __unsafe_unretained id coll
9494
return paths;
9595
}
9696

97+
// Calculates longest increasing indexes set using O(n log n) complexity algorithm
98+
static NSIndexSet *longestIncreasingIndexes(const vector<IGListRecord> &newResultsArray)
99+
{
100+
NSUInteger count = newResultsArray.size();
101+
vector<NSUInteger> prevIndexes(count);
102+
vector<NSUInteger> indexes(count + 1);
103+
104+
NSUInteger length = 0;
105+
for (NSUInteger i = 0; i < count; i++) {
106+
// Binary search for the largest positive j ≤ length
107+
// such that X[M[j]] < X[i]
108+
NSUInteger lo = 1;
109+
NSUInteger hi = length;
110+
while (lo <= hi) {
111+
auto mid = lo + (hi - lo) / 2;
112+
if (newResultsArray[indexes[mid]].index < newResultsArray[i].index) {
113+
lo = mid + 1;
114+
} else {
115+
hi = mid - 1;
116+
}
117+
}
118+
119+
// After searching, lo is 1 greater than the
120+
// length of the longest prefix of X[i]
121+
auto newLength = lo;
122+
123+
// The predecessor of X[i] is the last index of
124+
// the subsequence of length newLength-1
125+
prevIndexes[i] = indexes[newLength - 1];
126+
indexes[newLength] = i;
127+
128+
if (newLength > length) {
129+
// If we found a subsequence longer than any we've
130+
// found yet, update L
131+
length = newLength;
132+
}
133+
}
134+
135+
// Reconstruct the longest increasing indexes
136+
NSMutableIndexSet *result = [NSMutableIndexSet new];
137+
auto k = indexes[length];
138+
for (NSUInteger i = 0; i < length; i++) {
139+
NSUInteger index = newResultsArray[k].index;
140+
141+
// Ignore inserted entries
142+
if (index != NSNotFound) {
143+
[result addIndex:index];
144+
}
145+
k = prevIndexes[k];
146+
}
147+
return result;
148+
}
149+
150+
static BOOL mapContainsMoves(const unordered_map<NSUInteger, NSUInteger> &movesMap, NSUInteger fromIndex, NSUInteger toIndex)
151+
{
152+
auto iter = movesMap.find(fromIndex);
153+
return iter != movesMap.end() && iter->second == toIndex;
154+
}
155+
156+
157+
class IGListMoveChecker {
158+
public:
159+
virtual bool isMove(const NSInteger oldIndex,
160+
const NSInteger newIndex,
161+
const NSInteger insertOffset,
162+
const NSInteger deleteOffset) {
163+
164+
return (oldIndex - deleteOffset + insertOffset) != newIndex;
165+
}
166+
167+
virtual ~IGListMoveChecker() {}
168+
};
169+
170+
class IGListOptimalMoveChecker : public IGListMoveChecker {
171+
172+
NSIndexSet *longestIncreasingSequence;
173+
174+
// track previously made moves
175+
unordered_map<NSUInteger, NSUInteger> moves;
176+
177+
// track offsets for previously made moves to identify unnecessary moves
178+
NSInteger movesOffset = 0;
179+
public:
180+
IGListOptimalMoveChecker(const vector<IGListRecord> &newResultsArray)
181+
: longestIncreasingSequence(
182+
// Calculate longest increasing indexes, identifying entries which do not require any moves
183+
longestIncreasingIndexes(newResultsArray))
184+
{}
185+
186+
virtual bool isMove(const NSInteger oldIndex,
187+
const NSInteger newIndex,
188+
const NSInteger insertOffset,
189+
const NSInteger deleteOffset) override {
190+
191+
auto oldIndexNoDeletes = oldIndex - deleteOffset;
192+
auto oldCorrectedIndex = oldIndexNoDeletes + insertOffset;
193+
194+
// 1. if the indexes match, ignore the index, else
195+
// 2. if the index requires no move and is not swap move, ignore the index, else
196+
// 3. if the index matches old index with moves offset, ignore the index, else
197+
if (oldCorrectedIndex != newIndex
198+
&& (![longestIncreasingSequence containsIndex:oldIndex] || mapContainsMoves(moves, newIndex + deleteOffset - insertOffset, oldIndexNoDeletes))
199+
&& ((oldCorrectedIndex + movesOffset) != newIndex)) {
200+
201+
// store move for later checks
202+
moves[oldIndex] = newIndex;
203+
204+
// track offset caused by the move
205+
movesOffset++;
206+
207+
return true;
208+
}
209+
210+
return false;
211+
}
212+
};
213+
97214
static id IGListDiffing(BOOL returnIndexPaths,
98215
NSInteger fromSection,
99216
NSInteger toSection,
@@ -262,6 +379,13 @@ static id IGListDiffing(BOOL returnIndexPaths,
262379
addIndexToMap(returnIndexPaths, fromSection, i, oldArray[i], oldMap);
263380
}
264381

382+
383+
aligned_union<0, IGListMoveChecker, IGListOptimalMoveChecker>::type moveCheckerBuf;
384+
385+
IGListMoveChecker *moveChecker = IGListExperimentEnabled(experiments, IGListExperimentOptimizedMoves)
386+
? new (&moveCheckerBuf) IGListOptimalMoveChecker(newResultsArray)
387+
: new (&moveCheckerBuf) IGListMoveChecker();
388+
265389
// reset and track offsets from inserted items to calculate where items have moved
266390
runningOffset = 0;
267391

@@ -280,10 +404,12 @@ static id IGListDiffing(BOOL returnIndexPaths,
280404
}
281405

282406
// calculate the offset and determine if there was a move
283-
// if the indexes match, ignore the index
284407
const NSInteger insertOffset = insertOffsets[i];
285408
const NSInteger deleteOffset = deleteOffsets[oldIndex];
286-
if ((oldIndex - deleteOffset + insertOffset) != i) {
409+
410+
if (moveChecker->isMove(oldIndex, i, insertOffset, deleteOffset)) {
411+
412+
// add move from old index to new index
287413
id move;
288414
if (returnIndexPaths) {
289415
NSIndexPath *from = [NSIndexPath indexPathForItem:oldIndex inSection:fromSection];
@@ -299,6 +425,8 @@ static id IGListDiffing(BOOL returnIndexPaths,
299425
addIndexToMap(returnIndexPaths, toSection, i, newArray[i], newMap);
300426
}
301427

428+
moveChecker->~IGListMoveChecker();
429+
302430
NSCAssert((oldCount + [mInserts count] - [mDeletes count]) == newCount,
303431
@"Sanity check failed applying %li inserts and %lu deletes to old count %lu equaling new count %li",
304432
(long)oldCount, (unsigned long)[mInserts count], (unsigned long)[mDeletes count], (long)newCount);

Source/Common/IGListExperiments.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ typedef NS_OPTIONS (NSInteger, IGListExperiment) {
2626
IGListExperimentFasterVisibleSectionController = 1 << 4,
2727
/// Test deduping item-level updates.
2828
IGListExperimentDedupeItemUpdates = 1 << 5,
29+
/// Test optimized moves to minimal required number
30+
IGListExperimentOptimizedMoves = 1 << 6,
2931
};
3032

3133
/**

0 commit comments

Comments
 (0)