Skip to content

Commit

Permalink
Ruthlessly improve efficiency in ASRangeControllerBeta.
Browse files Browse the repository at this point in the history
- Use completedNodes directly, caching inner arrays and counts between loop iterations.
- Merge codepaths between the "entire self - table / collection" visible or invisible cases
- Ensure we do not trigger an assertion if a previous iteration's node is nil by the time
  we try to reset its interfaceState.
  • Loading branch information
Scott Goodson committed Jan 10, 2016
1 parent 0feaa2a commit 90a1bb2
Showing 1 changed file with 69 additions and 50 deletions.
119 changes: 69 additions & 50 deletions AsyncDisplayKit/Details/ASRangeControllerBeta.mm
Original file line number Diff line number Diff line change
Expand Up @@ -80,36 +80,48 @@ - (void)_updateVisibleNodeIndexPaths
[_layoutController setVisibleNodeIndexPaths:visibleNodePaths];
}

ASInterfaceState selfInterfaceState = [_dataSource interfaceStateForRangeController:self];
NSArray *allNodes = [_dataSource completedNodes];
NSArray *currentSectionNodes = nil;
NSInteger currentSectionIndex = -1; // Will be unequal to any indexPath.section, so we set currentSectionNodes.

NSSet *visibleIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible];
NSUInteger numberOfSections = [allNodes count];
NSUInteger numberOfNodesInSection = 0;

#if RangeControllerLoggingEnabled
NSMutableArray *modified = [NSMutableArray array];
#endif
NSSet *visibleIndexPaths = [NSSet setWithArray:visibleNodePaths];
// = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible];
NSSet *displayIndexPaths = nil;
NSSet *fetchDataIndexPaths = nil;
NSMutableSet *allIndexPaths = nil;
NSMutableArray *modifiedIndexPaths = (RangeControllerLoggingEnabled ? [NSMutableArray array] : nil);

ASInterfaceState selfInterfaceState = [_dataSource interfaceStateForRangeController:self];

if (ASInterfaceStateIncludesVisible(selfInterfaceState)) {
// If we are already visible, get busy! Better get started on preloading before the user scrolls more...
NSSet *fetchDataIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeFetchData];
NSSet *displayIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeDisplay];
fetchDataIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeFetchData];
displayIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeDisplay];

// Typically the fetchDataIndexPaths will be the largest, and be a superset of the others, though it may be disjoint.
NSMutableSet *allIndexPaths = [fetchDataIndexPaths mutableCopy];
allIndexPaths = [fetchDataIndexPaths mutableCopy];
[allIndexPaths unionSet:displayIndexPaths];
[allIndexPaths unionSet:visibleIndexPaths];
} else {
allIndexPaths = [visibleIndexPaths mutableCopy];
}

// Sets are magical. Add anything we had applied interfaceState to in the last update, so we can clear any
// range flags it still has enabled. Most of the time, all but a few elements are equal; a large programmatic
// scroll or major main thread stall could cause entirely disjoint sets, but we must visit all.
NSSet *allCurrentIndexPaths = [allIndexPaths copy];
[allIndexPaths unionSet:_allPreviousIndexPaths];
_allPreviousIndexPaths = allCurrentIndexPaths;

for (NSIndexPath *indexPath in allIndexPaths) {
// Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it.
// For consistency, make sure each node knows that it should measure itself if something changes.
ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout;

// Sets are magical. Add anything we had applied interfaceState to in the last update, so we can clear any
// range flags it still has enabled. Most of the time, all but a few elements are equal; a large programmatic
// scroll or major main thread stall could cause entirely disjoint sets, but we must visit all.
NSSet *allCurrentIndexPaths = [allIndexPaths copy];
[allIndexPaths unionSet:_allPreviousIndexPaths];
_allPreviousIndexPaths = allCurrentIndexPaths;

for (NSIndexPath *indexPath in allIndexPaths) {
// Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it.
// For consistency, make sure each node knows that it should measure itself if something changes.
ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout;

if (ASInterfaceStateIncludesVisible(selfInterfaceState)) {
if ([fetchDataIndexPaths containsObject:indexPath]) {
interfaceState |= ASInterfaceStateFetchData;
}
Expand All @@ -119,39 +131,49 @@ - (void)_updateVisibleNodeIndexPaths
if ([visibleIndexPaths containsObject:indexPath]) {
interfaceState |= ASInterfaceStateVisible;
}

ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath];
ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset.");
// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState.
if (node.interfaceState != interfaceState) {
#if RangeControllerLoggingEnabled
[modified addObject:indexPath];
#endif
[node recursivelySetInterfaceState:interfaceState];
} else {
// If selfInterfaceState isn't visible, then visibleIndexPaths represents what /will/ be immediately visible at the
// instant we come onscreen. So, fetch data and display all of those things, but don't waste resources preloading yet.
// We handle this as a separate case to minimize set operations for offscreen preloading, including containsObject:.

// Set Layout, Fetch Data, Display. DO NOT set Visible: even though these elements are in the visible range / "viewport",
// our overall container object is itself not visible yet. The moment it becomes visible, we will run the condition above.
if ([allCurrentIndexPaths containsObject:indexPath]) {
// We might be looking at an indexPath that was previously in-range, but now we need to clear it.
// In that case we'll just set it back to MeasureLayout. Only set Display | FetchData if in allCurrentIndexPaths.
interfaceState |= ASInterfaceStateDisplay;
interfaceState |= ASInterfaceStateFetchData;
}
}
} else {
// If selfInterfaceState isn't visible, then visibleIndexPaths represents what /will/ be immediately visible at the
// instant we come onscreen. So, fetch data and display all of those things, but don't waste resources preloading yet.
// We handle this as a separate case to minimize set operations for offscreen preloading, including containsObject:.

for (NSIndexPath *indexPath in visibleIndexPaths) {
// Set Layout, Fetch Data, Display. DO NOT set Visible: even though these elements are in the visible range / "viewport",
// our overall container object is itself not visible yet. The moment it becomes visible, we will run the condition above.
ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout | ASInterfaceStateFetchData | ASInterfaceStateDisplay;
NSInteger section = indexPath.section;
NSInteger row = indexPath.row;

if (section >= 0 && row >= 0 && section < numberOfSections) {
if (section != currentSectionIndex) {
// Often we'll be dealing with indexPaths in the same section, but the set isn't sorted and we may even bounce
// between the same ones. Still, this saves dozens of method calls to access the inner array and count.
currentSectionNodes = [allNodes objectAtIndex:section];
numberOfNodesInSection = [currentSectionNodes count];
currentSectionIndex = section;
}

ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath];
ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset.");
// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState.
if (node.interfaceState != interfaceState) {
#if RangeControllerLoggingEnabled
[modified addObject:indexPath];
#endif
[node recursivelySetInterfaceState:interfaceState];
if (row < numberOfNodesInSection) {
ASDisplayNode *node = [currentSectionNodes objectAtIndex:row];

ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset.");
// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState.
if (node.interfaceState != interfaceState) {
[modifiedIndexPaths addObject:indexPath];
[node recursivelySetInterfaceState:interfaceState];
}
}
}
}

_rangeIsValid = YES;
_queuedRangeUpdate = NO;

#if RangeControllerLoggingEnabled
NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths];
BOOL setsAreEqual = [visibleIndexPaths isEqualToSet:visibleNodePathsSet];
Expand All @@ -161,9 +183,9 @@ - (void)_updateVisibleNodeIndexPaths
NSLog(@"custom: %@", visibleNodePathsSet);
}

[modified sortUsingSelector:@selector(compare:)];
[modifiedIndexPaths sortUsingSelector:@selector(compare:)];

for (NSIndexPath *indexPath in modified) {
for (NSIndexPath *indexPath in modifiedIndexPaths) {
ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath];
ASInterfaceState interfaceState = node.interfaceState;
BOOL inVisible = ASInterfaceStateIncludesVisible(interfaceState);
Expand All @@ -172,9 +194,6 @@ - (void)_updateVisibleNodeIndexPaths
NSLog(@"indexPath %@, Visible: %d, Display: %d, FetchData: %d", indexPath, inVisible, inDisplay, inFetchData);
}
#endif

_rangeIsValid = YES;
_queuedRangeUpdate = NO;
}

#pragma mark - Cell node view handling
Expand Down

0 comments on commit 90a1bb2

Please sign in to comment.