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

[ASDisplayNode] Always layout nodes on a background thread #1907

Merged
merged 5 commits into from
Jul 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AsyncDisplayKit/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,7 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize
};

// TODO ihm: Can we always push the measure to the background thread and remove the parameter from the API?
if (shouldMeasureAsync) {
if (ASDisplayNodeThreadIsMain()) {
ASPerformBlockOnBackgroundThread(transitionBlock);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this code already checks if it is on a background thread and performs it inline if so - maybe not?

} else {
transitionBlock();
Expand Down
102 changes: 31 additions & 71 deletions AsyncDisplayKit/Details/ASDataController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,12 @@ - (void)_layoutNodes:(NSArray<ASCellNode *> *)nodes fromContexts:(NSArray<ASInde
return;
}

// For any given layout pass that occurs, this method will be called at least twice, once on the main thread and
// the background, to result in complete coverage of both loaded and unloaded nodes
BOOL isMainThread = ASDisplayNodeThreadIsMain();
// Layout node on whatever thread we are on. We handle the trampoline to the main thread in case the node is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you mean ASDisplayNode (rather than We = ASDataController) handles the trampoline?

// already loaded
for (NSUInteger k = range.location; k < NSMaxRange(range); k++) {
ASCellNode *node = nodes[k];
// Only nodes that are loaded should be layout on the main thread
if (node.isNodeLoaded == isMainThread) {
ASIndexedNodeContext *context = contexts[k];
[self _layoutNode:node withConstrainedSize:context.constrainedSize];
}
ASIndexedNodeContext *context = contexts[k];
[self _layoutNode:node withConstrainedSize:context.constrainedSize];
}
}

Expand All @@ -187,78 +183,42 @@ - (void)_layoutNodesFromContexts:(NSArray<ASIndexedNodeContext *> *)contexts ofK
}

NSUInteger nodeCount = contexts.count;
NSMutableArray<ASCellNode *> *allocatedNodes = [NSMutableArray<ASCellNode *> arrayWithCapacity:nodeCount];
dispatch_group_t layoutGroup = dispatch_group_create();
__strong NSIndexPath **allocatedContextIndexPaths = (__strong NSIndexPath **)calloc(nodeCount, sizeof(NSIndexPath *));
__strong ASCellNode **allocatedNodeBuffer = (__strong ASCellNode **)calloc(nodeCount, sizeof(ASCellNode *));

for (NSUInteger j = 0; j < nodeCount; j += kASDataControllerSizingCountPerProcessor) {
NSInteger batchCount = MIN(kASDataControllerSizingCountPerProcessor, nodeCount - j);

// Allocate nodes concurrently.
__block NSArray *subarrayOfContexts;
__block NSArray *subarrayOfNodes;
dispatch_block_t allocationBlock = ^{
__strong ASIndexedNodeContext **allocatedContextBuffer = (__strong ASIndexedNodeContext **)calloc(batchCount, sizeof(ASIndexedNodeContext *));
__strong ASCellNode **allocatedNodeBuffer = (__strong ASCellNode **)calloc(batchCount, sizeof(ASCellNode *));
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(batchCount, queue, ^(size_t i) {
unsigned long k = j + i;
ASIndexedNodeContext *context = contexts[k];
ASCellNode *node = [context allocateNode];
if (node == nil) {
ASDisplayNodeAssertNotNil(node, @"Node block created nil node; %@, %@", self, self.dataSource);
node = [[ASCellNode alloc] init]; // Fallback to avoid crash for production apps.
}
allocatedNodeBuffer[i] = node;
allocatedContextBuffer[i] = context;
});
subarrayOfNodes = [NSArray arrayWithObjects:allocatedNodeBuffer count:batchCount];
subarrayOfContexts = [NSArray arrayWithObjects:allocatedContextBuffer count:batchCount];
// Nil out buffer indexes to allow arc to free the stored cells.
for (int i = 0; i < batchCount; i++) {
allocatedContextBuffer[i] = nil;
allocatedNodeBuffer[i] = nil;

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(batchCount, queue, ^(size_t i) {
unsigned long k = j + i;
ASIndexedNodeContext *context = contexts[k];
ASCellNode *node = [context allocateNode];
if (node == nil) {
ASDisplayNodeAssertNotNil(node, @"Node block created nil node; %@, %@", self, self.dataSource);
node = [[ASCellNode alloc] init]; // Fallback to avoid crash for production apps.
}
free(allocatedContextBuffer);
free(allocatedNodeBuffer);
};

// Run the allocation block to concurrently create the cell nodes. Then, handle layout for nodes that are already loaded
// (e.g. the dataSource may have provided cells that have been used before), which must do layout on the main thread.
NSRange batchRange = NSMakeRange(0, batchCount);
if (ASDisplayNodeThreadIsMain()) {
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
allocationBlock();
dispatch_semaphore_signal(sema);
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

allocatedContextIndexPaths[k] = context.indexPath;
allocatedNodeBuffer[k] = node;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

later, seems like we don't need a separate node buffer if we have the contexts, if the context contains the allocated node (maybe there is a retain issue with this, not sure)


[self _layoutNodes:subarrayOfNodes fromContexts:subarrayOfContexts atIndexesOfRange:batchRange ofKind:kind];
} else {
allocationBlock();
[_mainSerialQueue performBlockOnMainThread:^{
[self _layoutNodes:subarrayOfNodes fromContexts:subarrayOfContexts atIndexesOfRange:batchRange ofKind:kind];
}];
}

[allocatedNodes addObjectsFromArray:subarrayOfNodes];

dispatch_group_async(layoutGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// We should already have measured loaded nodes before we left the main thread. Layout the remaining ones on a background thread.
NSRange asyncBatchRange = NSMakeRange(j, batchCount);
[self _layoutNodes:allocatedNodes fromContexts:contexts atIndexesOfRange:asyncBatchRange ofKind:kind];
[self _layoutNode:node withConstrainedSize:context.constrainedSize];
});
}

// Block the _editingTransactionQueue from executing a new edit transaction until layout is done & _editingNodes array is updated.
dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER);

if (completionBlock) {
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:nodeCount];
for (ASIndexedNodeContext *context in contexts) {
[indexPaths addObject:context.indexPath];
}
// Create nodes and indexPaths array's
NSArray *allocatedNodes = [NSArray arrayWithObjects:allocatedNodeBuffer count:nodeCount];
NSArray *indexPaths = [NSArray arrayWithObjects:allocatedContextIndexPaths count:nodeCount];

// Nil out buffer indexes to allow arc to free the stored cells.
for (int i = 0; i < nodeCount; i++) {
allocatedContextIndexPaths[i] = nil;
allocatedNodeBuffer[i] = nil;
}
free(allocatedContextIndexPaths);
free(allocatedNodeBuffer);

if (completionBlock) {
completionBlock(allocatedNodes, indexPaths);
}
}
Expand Down