Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Commit 516e1f1

Browse files
committed
Merge pull request #1053 from facebook/ASRangeControllerBetaOptimizations
Substantial optimizations for ASRangeControllerBeta & recursivelySetInterfaceState:.
2 parents 03d13b1 + 90a1bb2 commit 516e1f1

9 files changed

Lines changed: 172 additions & 97 deletions

File tree

AsyncDisplayKit.xcodeproj/project.pbxproj

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,13 @@
495495
remoteGlobalIDString = 058D09AB195D04C000B7D73C;
496496
remoteInfo = AsyncDisplayKit;
497497
};
498+
DEACA2B11C425DC400FA9DDF /* PBXContainerItemProxy */ = {
499+
isa = PBXContainerItemProxy;
500+
containerPortal = 058D09A4195D04C000B7D73C /* Project object */;
501+
proxyType = 1;
502+
remoteGlobalIDString = 058D09AB195D04C000B7D73C;
503+
remoteInfo = AsyncDisplayKit;
504+
};
498505
/* End PBXContainerItemProxy section */
499506

500507
/* Begin PBXCopyFilesBuildPhase section */
@@ -1524,6 +1531,7 @@
15241531
buildRules = (
15251532
);
15261533
dependencies = (
1534+
DEACA2B21C425DC400FA9DDF /* PBXTargetDependency */,
15271535
);
15281536
name = AsyncDisplayKitTestHost;
15291537
productName = AsyncDisplayKitTestHost;
@@ -1939,6 +1947,11 @@
19391947
target = 058D09AB195D04C000B7D73C /* AsyncDisplayKit */;
19401948
targetProxy = 058D09C2195D04C000B7D73C /* PBXContainerItemProxy */;
19411949
};
1950+
DEACA2B21C425DC400FA9DDF /* PBXTargetDependency */ = {
1951+
isa = PBXTargetDependency;
1952+
target = 058D09AB195D04C000B7D73C /* AsyncDisplayKit */;
1953+
targetProxy = DEACA2B11C425DC400FA9DDF /* PBXContainerItemProxy */;
1954+
};
19421955
/* End PBXTargetDependency section */
19431956

19441957
/* Begin PBXVariantGroup section */

AsyncDisplayKit.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

Lines changed: 0 additions & 8 deletions
This file was deleted.

AsyncDisplayKit/ASDisplayNode.mm

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -198,19 +198,22 @@ + (Class)layerClass
198198
return [_ASDisplayLayer class];
199199
}
200200

201-
+ (void)scheduleNodeForDisplay:(ASDisplayNode *)node
201+
+ (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node
202202
{
203203
ASDisplayNodeAssertMainThread();
204+
ASDisplayNodeAssert([ASDisplayNode shouldUseNewRenderingRange], @"+scheduleNodeForRecursiveDisplay: should never be called without the new rendering range enabled");
204205
static NSMutableSet *nodesToDisplay = nil;
205206
static BOOL displayScheduled = NO;
206207
static ASDN::RecursiveMutex displaySchedulerLock;
208+
207209
{
208210
ASDN::MutexLocker l(displaySchedulerLock);
209211
if (!nodesToDisplay) {
210212
nodesToDisplay = [[NSMutableSet alloc] init];
211213
}
212214
[nodesToDisplay addObject:node];
213215
}
216+
214217
if (!displayScheduled) {
215218
displayScheduled = YES;
216219
// It's essenital that any layout pass that is scheduled during the current
@@ -1557,13 +1560,11 @@ - (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously
15571560

15581561
- (void)setShouldBypassEnsureDisplay:(BOOL)shouldBypassEnsureDisplay
15591562
{
1560-
ASDN::MutexLocker l(_propertyLock);
15611563
_flags.shouldBypassEnsureDisplay = shouldBypassEnsureDisplay;
15621564
}
15631565

15641566
- (BOOL)shouldBypassEnsureDisplay
15651567
{
1566-
ASDN::MutexLocker l(_propertyLock);
15671568
return _flags.shouldBypassEnsureDisplay;
15681569
}
15691570

@@ -1612,7 +1613,7 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
16121613
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
16131614
{
16141615
ASDisplayNodeAssertThreadAffinity(self);
1615-
return [ASLayoutSpec new];
1616+
return nil;
16161617
}
16171618

16181619
- (ASLayout *)calculatedLayout
@@ -1772,7 +1773,7 @@ - (void)setInterfaceState:(ASInterfaceState)newState
17721773
oldState = _interfaceState;
17731774
_interfaceState = newState;
17741775
}
1775-
1776+
17761777
if ((newState & ASInterfaceStateMeasureLayout) != (oldState & ASInterfaceStateMeasureLayout)) {
17771778
// Trigger asynchronous measurement if it is not already cached or being calculated.
17781779
}
@@ -1782,8 +1783,11 @@ - (void)setInterfaceState:(ASInterfaceState)newState
17821783
// Still, the interfaceState should be updated to the current state of the node; just don't act on the transition.
17831784

17841785
// Entered or exited data loading state.
1785-
if ((newState & ASInterfaceStateFetchData) != (oldState & ASInterfaceStateFetchData)) {
1786-
if (newState & ASInterfaceStateFetchData) {
1786+
BOOL nowFetchData = ASInterfaceStateIncludesFetchData(newState);
1787+
BOOL wasFetchData = ASInterfaceStateIncludesFetchData(oldState);
1788+
1789+
if (nowFetchData != wasFetchData) {
1790+
if (nowFetchData) {
17871791
[self fetchData];
17881792
} else {
17891793
if ([self supportsRangeManagedInterfaceState]) {
@@ -1793,21 +1797,42 @@ - (void)setInterfaceState:(ASInterfaceState)newState
17931797
}
17941798

17951799
// Entered or exited contents rendering state.
1796-
if ((newState & ASInterfaceStateDisplay) != (oldState & ASInterfaceStateDisplay)) {
1800+
BOOL nowDisplay = ASInterfaceStateIncludesDisplay(newState);
1801+
BOOL wasDisplay = ASInterfaceStateIncludesDisplay(oldState);
1802+
1803+
if (nowDisplay != wasDisplay) {
17971804
if ([self supportsRangeManagedInterfaceState]) {
1798-
if (newState & ASInterfaceStateDisplay) {
1805+
if (nowDisplay) {
17991806
// Once the working window is eliminated (ASRangeHandlerRender), trigger display directly here.
18001807
[self setDisplaySuspended:NO];
18011808
} else {
18021809
[self setDisplaySuspended:YES];
18031810
[self clearContents];
18041811
}
1812+
} else {
1813+
// NOTE: This case isn't currently supported as setInterfaceState: isn't exposed externally, and all
1814+
// internal use cases are range-managed. When a node is visible, don't mess with display - CA will start it.
1815+
if ([ASDisplayNode shouldUseNewRenderingRange] && !ASInterfaceStateIncludesVisible(newState)) {
1816+
// Check __implementsDisplay purely for efficiency - it's faster even than calling -asyncLayer.
1817+
if ([self __implementsDisplay]) {
1818+
if (nowDisplay) {
1819+
[ASDisplayNode scheduleNodeForRecursiveDisplay:self];
1820+
} else {
1821+
[[self asyncLayer] cancelAsyncDisplay];
1822+
[self clearContents];
1823+
}
1824+
}
1825+
}
18051826
}
18061827
}
18071828

1808-
// Entered or exited data loading state.
1809-
if ((newState & ASInterfaceStateVisible) != (oldState & ASInterfaceStateVisible)) {
1810-
if (newState & ASInterfaceStateVisible) {
1829+
// Became visible or invisible. When range-managed, this represents literal visibility - at least one pixel
1830+
// is onscreen. If not range-managed, we can't guarantee more than the node being present in an onscreen window.
1831+
BOOL nowVisible = ASInterfaceStateIncludesVisible(newState);
1832+
BOOL wasVisible = ASInterfaceStateIncludesVisible(oldState);
1833+
1834+
if (nowVisible != wasVisible) {
1835+
if (nowVisible) {
18111836
[self visibilityDidChange:YES];
18121837
} else {
18131838
[self visibilityDidChange:NO];
@@ -1843,11 +1868,23 @@ - (void)exitInterfaceState:(ASInterfaceState)interfaceState
18431868

18441869
- (void)recursivelySetInterfaceState:(ASInterfaceState)interfaceState
18451870
{
1871+
ASInterfaceState oldState = self.interfaceState;
1872+
ASInterfaceState newState = interfaceState;
18461873
ASDisplayNodePerformBlockOnEveryNode(nil, self, ^(ASDisplayNode *node) {
18471874
node.interfaceState = interfaceState;
18481875
});
1849-
// FIXME: This should also be called in setInterfaceState: if it isn't being applied recursively.
1850-
[ASDisplayNode scheduleNodeForDisplay:self];
1876+
1877+
if ([self supportsRangeManagedInterfaceState]) {
1878+
// Instead of each node in the recursion assuming it needs to schedule itself for display,
1879+
// setInterfaceState: skips this when handling range-managed nodes (our whole subtree has this set).
1880+
// If our range manager intends for us to be displayed right now, and didn't before, get started!
1881+
1882+
BOOL nowDisplay = ASInterfaceStateIncludesDisplay(newState);
1883+
BOOL wasDisplay = ASInterfaceStateIncludesDisplay(oldState);
1884+
if (nowDisplay && (nowDisplay != wasDisplay)) {
1885+
[ASDisplayNode scheduleNodeForRecursiveDisplay:self];
1886+
}
1887+
}
18511888
}
18521889

18531890
- (ASHierarchyState)hierarchyState

AsyncDisplayKit/ASDisplayNodeExtras.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@
1212
#import <AsyncDisplayKit/ASBaseDefines.h>
1313
#import <AsyncDisplayKit/ASDisplayNode.h>
1414

15+
// Because inline methods can't be extern'd and need to be part of the translation unit of code
16+
// that compiles with them to actually inline, we both declare and define these in the header.
17+
inline BOOL ASInterfaceStateIncludesVisible(ASInterfaceState interfaceState)
18+
{
19+
return ((interfaceState & ASInterfaceStateVisible) == ASInterfaceStateVisible);
20+
}
21+
22+
inline BOOL ASInterfaceStateIncludesDisplay(ASInterfaceState interfaceState)
23+
{
24+
return ((interfaceState & ASInterfaceStateDisplay) == ASInterfaceStateDisplay);
25+
}
26+
27+
inline BOOL ASInterfaceStateIncludesFetchData(ASInterfaceState interfaceState)
28+
{
29+
return ((interfaceState & ASInterfaceStateFetchData) == ASInterfaceStateFetchData);
30+
}
31+
1532
NS_ASSUME_NONNULL_BEGIN
1633

1734
ASDISPLAYNODE_EXTERN_C_BEGIN

AsyncDisplayKit/Details/ASRangeControllerBeta.mm

Lines changed: 69 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,6 @@
1717
#import "ASInternalHelpers.h"
1818
#import "ASDisplayNode+FrameworkPrivate.h"
1919

20-
extern BOOL ASInterfaceStateIncludesVisible(ASInterfaceState interfaceState)
21-
{
22-
return ((interfaceState & ASInterfaceStateVisible) == ASInterfaceStateVisible);
23-
}
24-
25-
extern BOOL ASInterfaceStateIncludesDisplay(ASInterfaceState interfaceState)
26-
{
27-
return ((interfaceState & ASInterfaceStateDisplay) == ASInterfaceStateDisplay);
28-
}
29-
30-
extern BOOL ASInterfaceStateIncludesFetchData(ASInterfaceState interfaceState)
31-
{
32-
return ((interfaceState & ASInterfaceStateFetchData) == ASInterfaceStateFetchData);
33-
}
34-
3520
@interface ASRangeControllerBeta ()
3621
{
3722
BOOL _rangeIsValid;
@@ -95,36 +80,48 @@ - (void)_updateVisibleNodeIndexPaths
9580
[_layoutController setVisibleNodeIndexPaths:visibleNodePaths];
9681
}
9782

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

100-
NSSet *visibleIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible];
87+
NSUInteger numberOfSections = [allNodes count];
88+
NSUInteger numberOfNodesInSection = 0;
10189

102-
#if RangeControllerLoggingEnabled
103-
NSMutableArray *modified = [NSMutableArray array];
104-
#endif
90+
NSSet *visibleIndexPaths = [NSSet setWithArray:visibleNodePaths];
91+
// = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible];
92+
NSSet *displayIndexPaths = nil;
93+
NSSet *fetchDataIndexPaths = nil;
94+
NSMutableSet *allIndexPaths = nil;
95+
NSMutableArray *modifiedIndexPaths = (RangeControllerLoggingEnabled ? [NSMutableArray array] : nil);
96+
97+
ASInterfaceState selfInterfaceState = [_dataSource interfaceStateForRangeController:self];
10598

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

111104
// Typically the fetchDataIndexPaths will be the largest, and be a superset of the others, though it may be disjoint.
112-
NSMutableSet *allIndexPaths = [fetchDataIndexPaths mutableCopy];
105+
allIndexPaths = [fetchDataIndexPaths mutableCopy];
113106
[allIndexPaths unionSet:displayIndexPaths];
114107
[allIndexPaths unionSet:visibleIndexPaths];
108+
} else {
109+
allIndexPaths = [visibleIndexPaths mutableCopy];
110+
}
111+
112+
// Sets are magical. Add anything we had applied interfaceState to in the last update, so we can clear any
113+
// range flags it still has enabled. Most of the time, all but a few elements are equal; a large programmatic
114+
// scroll or major main thread stall could cause entirely disjoint sets, but we must visit all.
115+
NSSet *allCurrentIndexPaths = [allIndexPaths copy];
116+
[allIndexPaths unionSet:_allPreviousIndexPaths];
117+
_allPreviousIndexPaths = allCurrentIndexPaths;
118+
119+
for (NSIndexPath *indexPath in allIndexPaths) {
120+
// Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it.
121+
// For consistency, make sure each node knows that it should measure itself if something changes.
122+
ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout;
115123

116-
// Sets are magical. Add anything we had applied interfaceState to in the last update, so we can clear any
117-
// range flags it still has enabled. Most of the time, all but a few elements are equal; a large programmatic
118-
// scroll or major main thread stall could cause entirely disjoint sets, but we must visit all.
119-
NSSet *allCurrentIndexPaths = [allIndexPaths copy];
120-
[allIndexPaths unionSet:_allPreviousIndexPaths];
121-
_allPreviousIndexPaths = allCurrentIndexPaths;
122-
123-
for (NSIndexPath *indexPath in allIndexPaths) {
124-
// Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it.
125-
// For consistency, make sure each node knows that it should measure itself if something changes.
126-
ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout;
127-
124+
if (ASInterfaceStateIncludesVisible(selfInterfaceState)) {
128125
if ([fetchDataIndexPaths containsObject:indexPath]) {
129126
interfaceState |= ASInterfaceStateFetchData;
130127
}
@@ -134,39 +131,49 @@ - (void)_updateVisibleNodeIndexPaths
134131
if ([visibleIndexPaths containsObject:indexPath]) {
135132
interfaceState |= ASInterfaceStateVisible;
136133
}
137-
138-
ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath];
139-
ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset.");
140-
// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState.
141-
if (node.interfaceState != interfaceState) {
142-
#if RangeControllerLoggingEnabled
143-
[modified addObject:indexPath];
144-
#endif
145-
[node recursivelySetInterfaceState:interfaceState];
134+
} else {
135+
// If selfInterfaceState isn't visible, then visibleIndexPaths represents what /will/ be immediately visible at the
136+
// instant we come onscreen. So, fetch data and display all of those things, but don't waste resources preloading yet.
137+
// We handle this as a separate case to minimize set operations for offscreen preloading, including containsObject:.
138+
139+
// Set Layout, Fetch Data, Display. DO NOT set Visible: even though these elements are in the visible range / "viewport",
140+
// our overall container object is itself not visible yet. The moment it becomes visible, we will run the condition above.
141+
if ([allCurrentIndexPaths containsObject:indexPath]) {
142+
// We might be looking at an indexPath that was previously in-range, but now we need to clear it.
143+
// In that case we'll just set it back to MeasureLayout. Only set Display | FetchData if in allCurrentIndexPaths.
144+
interfaceState |= ASInterfaceStateDisplay;
145+
interfaceState |= ASInterfaceStateFetchData;
146146
}
147147
}
148-
} else {
149-
// If selfInterfaceState isn't visible, then visibleIndexPaths represents what /will/ be immediately visible at the
150-
// instant we come onscreen. So, fetch data and display all of those things, but don't waste resources preloading yet.
151-
// We handle this as a separate case to minimize set operations for offscreen preloading, including containsObject:.
152148

153-
for (NSIndexPath *indexPath in visibleIndexPaths) {
154-
// Set Layout, Fetch Data, Display. DO NOT set Visible: even though these elements are in the visible range / "viewport",
155-
// our overall container object is itself not visible yet. The moment it becomes visible, we will run the condition above.
156-
ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout | ASInterfaceStateFetchData | ASInterfaceStateDisplay;
149+
NSInteger section = indexPath.section;
150+
NSInteger row = indexPath.row;
151+
152+
if (section >= 0 && row >= 0 && section < numberOfSections) {
153+
if (section != currentSectionIndex) {
154+
// Often we'll be dealing with indexPaths in the same section, but the set isn't sorted and we may even bounce
155+
// between the same ones. Still, this saves dozens of method calls to access the inner array and count.
156+
currentSectionNodes = [allNodes objectAtIndex:section];
157+
numberOfNodesInSection = [currentSectionNodes count];
158+
currentSectionIndex = section;
159+
}
157160

158-
ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath];
159-
ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset.");
160-
// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState.
161-
if (node.interfaceState != interfaceState) {
162-
#if RangeControllerLoggingEnabled
163-
[modified addObject:indexPath];
164-
#endif
165-
[node recursivelySetInterfaceState:interfaceState];
161+
if (row < numberOfNodesInSection) {
162+
ASDisplayNode *node = [currentSectionNodes objectAtIndex:row];
163+
164+
ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset.");
165+
// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState.
166+
if (node.interfaceState != interfaceState) {
167+
[modifiedIndexPaths addObject:indexPath];
168+
[node recursivelySetInterfaceState:interfaceState];
169+
}
166170
}
167171
}
168172
}
169173

174+
_rangeIsValid = YES;
175+
_queuedRangeUpdate = NO;
176+
170177
#if RangeControllerLoggingEnabled
171178
NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths];
172179
BOOL setsAreEqual = [visibleIndexPaths isEqualToSet:visibleNodePathsSet];
@@ -176,9 +183,9 @@ - (void)_updateVisibleNodeIndexPaths
176183
NSLog(@"custom: %@", visibleNodePathsSet);
177184
}
178185

179-
[modified sortUsingSelector:@selector(compare:)];
186+
[modifiedIndexPaths sortUsingSelector:@selector(compare:)];
180187

181-
for (NSIndexPath *indexPath in modified) {
188+
for (NSIndexPath *indexPath in modifiedIndexPaths) {
182189
ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath];
183190
ASInterfaceState interfaceState = node.interfaceState;
184191
BOOL inVisible = ASInterfaceStateIncludesVisible(interfaceState);
@@ -187,9 +194,6 @@ - (void)_updateVisibleNodeIndexPaths
187194
NSLog(@"indexPath %@, Visible: %d, Display: %d, FetchData: %d", indexPath, inVisible, inDisplay, inFetchData);
188195
}
189196
#endif
190-
191-
_rangeIsValid = YES;
192-
_queuedRangeUpdate = NO;
193197
}
194198

195199
#pragma mark - Cell node view handling

0 commit comments

Comments
 (0)