Skip to content

Commit a72a4c8

Browse files
nguyenhuyOhKanghoon
authored andcommitted
More on ASDataController's main-thread-only mode (TextureGroup#1915)
Follow up on TextureGroup#1911: it's not enough to execute step 3 on the main thread because -_allocateNodesFromElements: uses ASDispatchApply to offload the work to other threads. So this diff adds a flag to tell that method to do everything serially on the calling thread.
1 parent d6f16a2 commit a72a4c8

1 file changed

Lines changed: 35 additions & 12 deletions

File tree

Source/Details/ASDataController.mm

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,16 @@ - (void)setLayoutDelegate:(id<ASDataControllerLayoutDelegate>)layoutDelegate
129129

130130
#pragma mark - Cell Layout
131131

132+
/**
133+
* Allocates and layouts nodes from the given collection elements, and blocks the current thread while doing so.
134+
*
135+
* @param elements The elements from which nodes can be allocated and laid out.
136+
* @param strictlyOnCurrentThread Whether or not all the work must be done strictly on the current thread.
137+
* YES means all nodes will be allocated and laid out serially on the current thread.
138+
* NO means the work can be offloaded to other thread(s), potentially reduce the blocking time on the calling thread.
139+
*/
132140
- (void)_allocateNodesFromElements:(NSArray<ASCollectionElement *> *)elements
141+
strictlyOnCurrentThread:(BOOL)strictlyOnCurrentThread
133142
{
134143
NSUInteger nodeCount = elements.count;
135144
__weak id<ASDataControllerSource> weakDataSource = _dataSource;
@@ -142,12 +151,7 @@ - (void)_allocateNodesFromElements:(NSArray<ASCollectionElement *> *)elements
142151
{
143152
as_activity_create_for_scope("Data controller batch");
144153

145-
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
146-
NSUInteger threadCount = 0;
147-
if ([_dataSource dataControllerShouldSerializeNodeCreation:self]) {
148-
threadCount = 1;
149-
}
150-
ASDispatchApply(nodeCount, queue, threadCount, ^(size_t i) {
154+
void(^work)(size_t) = ^(size_t i) {
151155
__strong id<ASDataControllerSource> strongDataSource = weakDataSource;
152156
if (strongDataSource == nil) {
153157
return;
@@ -166,7 +170,20 @@ - (void)_allocateNodesFromElements:(NSArray<ASCollectionElement *> *)elements
166170
if (ASSizeRangeHasSignificantArea(sizeRange)) {
167171
[self _layoutNode:node withConstrainedSize:sizeRange];
168172
}
169-
});
173+
};
174+
175+
if (strictlyOnCurrentThread) {
176+
for (NSUInteger i = 0; i < nodeCount; i++) {
177+
work(i);
178+
}
179+
} else {
180+
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
181+
NSUInteger threadCount = 0;
182+
if ([_dataSource dataControllerShouldSerializeNodeCreation:self]) {
183+
threadCount = 1;
184+
}
185+
ASDispatchApply(nodeCount, queue, threadCount, work);
186+
}
170187
}
171188

172189
ASSignpostEnd(DataControllerBatch, self, "count: %lu", (unsigned long)nodeCount);
@@ -620,8 +637,11 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet
620637
Class<ASDataControllerLayoutDelegate> layoutDelegateClass = [self.layoutDelegate class];
621638

622639
// Step 3: Call the layout delegate if possible. Otherwise, allocate and layout all elements
623-
dispatch_block_t step3 = ^{
640+
void (^step3)(BOOL) = ^(BOOL strictlyOnCurrentThread){
624641
if (canDelegate) {
642+
// Don't pass strictlyOnCurrentThread to the layout delegate. Instead give it
643+
// total control over its threading behavior, as long as it blocks the
644+
// calling thread while preparing the layout (which is part of the API contract).
625645
[layoutDelegateClass calculateLayoutWithContext:layoutContext];
626646
} else {
627647
const auto elementsToProcess = [[NSMutableArray<ASCollectionElement *> alloc] init];
@@ -635,21 +655,24 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet
635655
[elementsToProcess addObject:element];
636656
}
637657
}
638-
[self _allocateNodesFromElements:elementsToProcess];
658+
[self _allocateNodesFromElements:elementsToProcess
659+
strictlyOnCurrentThread:strictlyOnCurrentThread];
639660
}
640661
};
641662

642663
// Step 3 can be done on the main thread or on _editingTransactionQueue
643664
// depending on an experiment.
644665
BOOL mainThreadOnly = ASActivateExperimentalFeature(ASExperimentalMainThreadOnlyDataController);
645666
if (mainThreadOnly) {
646-
// We'll still dispatch to _editingTransactionQueue only to schedule a block
667+
// In main-thread-only mode allocate and layout all nodes serially on the main thread.
668+
//
669+
// After this step, we'll still dispatch to _editingTransactionQueue only to schedule a block
647670
// to _mainSerialQueue to execute next steps. This is not optimized because
648671
// in theory we can skip _editingTransactionQueue entirely, but it's much safer
649672
// because change sets will still flow through the pipeline in pretty the same way
650673
// (main thread -> _editingTransactionQueue -> _mainSerialQueue) and so
651674
// any methods that block on _editingTransactionQueue will still work.
652-
step3();
675+
step3(YES);
653676
}
654677

655678
++_editingTransactionGroupCount;
@@ -658,7 +681,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet
658681
as_activity_scope_enter(as_activity_create("Prepare nodes for collection update", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT), &preparationScope);
659682

660683
if (!mainThreadOnly) {
661-
step3();
684+
step3(NO);
662685
}
663686

664687
// Step 4: Inform the delegate on main thread

0 commit comments

Comments
 (0)