Skip to content

Commit

Permalink
Add ASDispatchAsync and use it in ASCollectionLayout
Browse files Browse the repository at this point in the history
  • Loading branch information
nguyenhuy committed Jul 14, 2017
1 parent 1a643cb commit 5950a9f
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 48 deletions.
4 changes: 4 additions & 0 deletions AsyncDisplayKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@
E5B078001E69F4EB00C24B5B /* ASElementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */; };
E5B225281F1790D6001E1431 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B225271F1790B5001E1431 /* ASHashing.h */; settings = {ATTRIBUTES = (Public, ); }; };
E5B225291F1790EE001E1431 /* ASHashing.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B225261F1790B5001E1431 /* ASHashing.m */; };
E5B2252E1F17E521001E1431 /* ASDispatch.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B2252D1F17E521001E1431 /* ASDispatch.m */; };
E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */; settings = {ATTRIBUTES = (Private, ); }; };
E5C347B11ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */; };
E5C347B31ECB40AA00EC4BE4 /* ASTableNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */; };
Expand Down Expand Up @@ -936,6 +937,7 @@
E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASElementMap.m; sourceTree = "<group>"; };
E5B225261F1790B5001E1431 /* ASHashing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASHashing.m; sourceTree = "<group>"; };
E5B225271F1790B5001E1431 /* ASHashing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASHashing.h; sourceTree = "<group>"; };
E5B2252D1F17E521001E1431 /* ASDispatch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASDispatch.m; sourceTree = "<group>"; };
E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionLayoutContext+Private.h"; sourceTree = "<group>"; };
E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchFetchingDelegate.h; sourceTree = "<group>"; };
E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASTableNode+Beta.h"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1378,6 +1380,7 @@
AEB7B0181C5962EA00662EF4 /* ASDefaultPlayButton.h */,
AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.m */,
CC54A81B1D70077A00296A24 /* ASDispatch.h */,
E5B2252D1F17E521001E1431 /* ASDispatch.m */,
058D0A08195D050800B7D73C /* ASDisplayNode+AsyncDisplay.mm */,
058D0A09195D050800B7D73C /* ASDisplayNode+DebugTiming.h */,
058D0A0A195D050800B7D73C /* ASDisplayNode+DebugTiming.mm */,
Expand Down Expand Up @@ -2309,6 +2312,7 @@
E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */,
34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */,
7AB338661C55B3420055FDE8 /* ASRelativeLayoutSpec.mm in Sources */,
E5B2252E1F17E521001E1431 /* ASDispatch.m in Sources */,
696F01EE1DD2AF450049FBD5 /* ASEventLog.mm in Sources */,
9C70F2051CDA4F06007D6C76 /* ASTraitCollection.m in Sources */,
83A7D95B1D44547700BF333E /* ASWeakMap.m in Sources */,
Expand Down
8 changes: 4 additions & 4 deletions Source/Details/ASCollectionLayoutState.mm
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,11 @@ - (ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTab
}

// Step 2: Filter out attributes in these pages that intersect the specified rect.
ASPageToLayoutAttributesTable *result = [ASPageTable pageTableForStrongObjectPointers];
ASPageToLayoutAttributesTable *result = nil;
for (id pagePtr in pagesInRect) {
ASPageCoordinate page = (ASPageCoordinate)pagePtr;
NSMutableArray *attrsInPage = [_unmeasuredPageToLayoutAttributesTable objectForPage:page];

if (attrsInPage.count == 0) {
// Hm, this page should have been removed.
[_unmeasuredPageToLayoutAttributesTable removeObjectForPage:page];
continue;
}

Expand All @@ -190,6 +187,9 @@ - (ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTab
} else {
[attrsInPage removeObjectsInArray:intersectingAttrsInPage];
}
if (result == nil) {
result = [ASPageTable pageTableForStrongObjectPointers];
}
[result setObject:intersectingAttrsInPage forPage:page];
}
}
Expand Down
37 changes: 17 additions & 20 deletions Source/Private/ASCollectionLayout.mm
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,9 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la
}

// Step 3: Split all those attributes into blocking and non-blocking buckets
// Use an ordered set here because some items may span multiple pages and they will be accessed by indexes.
// Use ordered sets here because some items may span multiple pages, and the sets will be accessed by indexes later on.
NSMutableOrderedSet<UICollectionViewLayoutAttributes *> *blockingAttrs = hasBlockingRect ? [NSMutableOrderedSet orderedSet] : nil;
// Use a set here because some items may span multiple pages
NSMutableSet<UICollectionViewLayoutAttributes *> *nonBlockingAttrs = [NSMutableSet set];
NSMutableOrderedSet<UICollectionViewLayoutAttributes *> *nonBlockingAttrs = [NSMutableOrderedSet orderedSet];
for (id pagePtr in attrsTable) {
ASPageCoordinate page = (ASPageCoordinate)pagePtr;
NSArray<UICollectionViewLayoutAttributes *> *attrsInPage = [attrsTable objectForPage:page];
Expand Down Expand Up @@ -290,30 +289,28 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la
// Step 4: Allocate and measure blocking elements' node
ASElementMap *elements = context.elements;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSUInteger count = blockingAttrs.count;
if (count > 0) {
if (NSUInteger count = blockingAttrs.count) {
ASDispatchApply(count, queue, 0, ^(size_t i) {
UICollectionViewLayoutAttributes *attrs = blockingAttrs[i];
CGSize elementSize = attrs.frame.size;
ASCollectionElement *element = [elements elementForItemAtIndexPath:attrs.indexPath];
ASCellNode *node = element.node;
if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) {
[node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)];
ASCellNode *node = [elements elementForItemAtIndexPath:attrs.indexPath].node;
CGSize expectedSize = attrs.frame.size;
if (! CGSizeEqualToSize(expectedSize, node.calculatedSize)) {
[node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(expectedSize)];
}
});
}

// Step 5: Allocate and measure non-blocking ones
// TODO Limit the number of threads
for (UICollectionViewLayoutAttributes *attrs in nonBlockingAttrs) {
CGSize elementSize = attrs.frame.size;
__weak ASCollectionElement *weakElement = [elements elementForItemAtIndexPath:attrs.indexPath];
dispatch_async(queue, ^{
__strong ASCollectionElement *strongElement = weakElement;
if (strongElement) {
ASCellNode *node = strongElement.node;
if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) {
[node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)];
if (NSUInteger count = nonBlockingAttrs.count) {
__weak ASElementMap *weakElements = elements;
ASDispatchAsync(count, queue, 0, ^(size_t i) {
__strong ASElementMap *strongElements = weakElements;
if (strongElements) {
UICollectionViewLayoutAttributes *attrs = nonBlockingAttrs[i];
ASCellNode *node = [elements elementForItemAtIndexPath:attrs.indexPath].node;
CGSize expectedSize = attrs.frame.size;
if (! CGSizeEqualToSize(expectedSize, node.calculatedSize)) {
[node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(expectedSize)];
}
}
});
Expand Down
36 changes: 14 additions & 22 deletions Source/Private/ASDispatch.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,24 @@
//

#import <Foundation/Foundation.h>
#import <stdatomic.h>
#import <AsyncDisplayKit/ASBaseDefines.h>

ASDISPLAYNODE_EXTERN_C_BEGIN

/**
* Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
static void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) {
if (threadCount == 0) {
threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2;
}
dispatch_group_t group = dispatch_group_create();
// HACK: This is a workaround for mm files that include this in Clang4.0
// Omitting ATOMIC_VAR_INIT is okay in this case because the current
// expansion of that macro no-ops.
// TODO: Move this implementation into a m file so it's not compiled in C++
// See: https://github.com/TextureGroup/Texture/pull/426
__block atomic_size_t counter = 0;
for (NSUInteger t = 0; t < threadCount; t++) {
dispatch_group_async(group, queue, ^{
size_t i;
while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) {
work(i);
}
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
};
void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i));

/**
* Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i));

ASDISPLAYNODE_EXTERN_C_END
59 changes: 59 additions & 0 deletions Source/Private/ASDispatch.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// ASDispatch.m
// Texture
//
// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//

#import <AsyncDisplayKit/ASDispatch.h>
#import <stdatomic.h>

/**
* Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) {
if (threadCount == 0) {
threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2;
}
dispatch_group_t group = dispatch_group_create();
__block atomic_size_t counter = ATOMIC_VAR_INIT(0);
for (NSUInteger t = 0; t < threadCount; t++) {
dispatch_group_async(group, queue, ^{
size_t i;
while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) {
work(i);
}
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
};

/**
* Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) {
if (threadCount == 0) {
threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2;
}
__block atomic_size_t counter = ATOMIC_VAR_INIT(0);
for (NSUInteger t = 0; t < threadCount; t++) {
dispatch_async(queue, ^{
size_t i;
while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) {
work(i);
}
});
}
};

38 changes: 36 additions & 2 deletions Tests/ASDispatchTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@
// ASDispatchTests.m
// Texture
//
// Created by Adlai Holler on 8/25/16.
// Copyright © 2016 Facebook. All rights reserved.
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// grant of patent rights can be found in the PATENTS file in the same directory.
//
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//

#import <XCTest/XCTest.h>
Expand Down Expand Up @@ -35,4 +44,29 @@ - (void)testDispatchApply
XCTAssertEqualObjects(indices, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, iterations)]);
}

- (void)testDispatchAsync
{
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSInteger expectedThreadCount = [NSProcessInfo processInfo].activeProcessorCount * 2;
NSLock *lock = [NSLock new];
NSMutableSet *threads = [NSMutableSet set];
NSMutableIndexSet *indices = [NSMutableIndexSet indexSet];
XCTestExpectation *expectation = [self expectationWithDescription:@"Executed all blocks"];

size_t const iterations = 1E5;
ASDispatchAsync(iterations, q, 0, ^(size_t i) {
[lock lock];
[threads addObject:[NSThread currentThread]];
XCTAssertFalse([indices containsIndex:i]);
[indices addIndex:i];
if (indices.count == iterations) {
[expectation fulfill];
}
[lock unlock];
});
[self waitForExpectationsWithTimeout:10 handler:nil];
XCTAssertLessThanOrEqual(threads.count, expectedThreadCount);
XCTAssertEqualObjects(indices, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, iterations)]);
}

@end

0 comments on commit 5950a9f

Please sign in to comment.