[_ASAsyncTransaction] implement operation priority

Signed-off-by: Matej Knopp <matej.knopp@gmail.com>
This commit is contained in:
Matej Knopp 2016-01-26 15:31:40 +01:00
parent 75abf07c6f
commit 0a45bd9596
6 changed files with 168 additions and 14 deletions

View File

@ -66,6 +66,11 @@ typedef NS_OPTIONS(NSUInteger, ASInterfaceState)
ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStateFetchData | ASInterfaceStateDisplay | ASInterfaceStateVisible,
};
/**
* Default drawing priority for display node
*/
extern NSUInteger const ASDefaultDrawingPriority;
/**
* An `ASDisplayNode` is an abstraction over `UIView` and `CALayer` that allows you to perform calculations about a view
* hierarchy off the main thread, and could do rendering off the main thread as well.
@ -475,6 +480,13 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, assign) NSTimeInterval placeholderFadeDuration;
/**
* @abstract Determines drawing priority of the node. Nodes with higher priority will be drawn earlier.
*
* @discussion Defaults to ASDefaultDrawingPriority. There may be multiple drawing threads, and some of them may
* decide to perform operations in queued order (regardless of drawingPriority)
*/
@property (nonatomic, assign) NSUInteger drawingPriority;
/** @name Hit Testing */

View File

@ -26,6 +26,8 @@
#import "ASLayoutSpec.h"
#import "ASCellNode.h"
NSUInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority;
@interface ASDisplayNode () <UIGestureRecognizerDelegate>
/**
@ -2236,6 +2238,31 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer,
}
}
static const char *ASDisplayNodeDrawingPriorityKey = "ASDrawingPriority";
- (void)setDrawingPriority:(NSUInteger)drawingPriority
{
ASDisplayNodeAssertThreadAffinity(self);
ASDN::MutexLocker l(_propertyLock);
if (drawingPriority == ASDefaultDrawingPriority) {
_flags.hasCustomDrawingPriority = NO;
objc_setAssociatedObject(self, ASDisplayNodeDrawingPriorityKey, nil, OBJC_ASSOCIATION_ASSIGN);
} else {
_flags.hasCustomDrawingPriority = YES;
objc_setAssociatedObject(self, ASDisplayNodeDrawingPriorityKey, [NSNumber numberWithUnsignedInteger:drawingPriority], OBJC_ASSOCIATION_RETAIN);
}
}
-(NSUInteger)drawingPriority
{
ASDisplayNodeAssertThreadAffinity(self);
ASDN::MutexLocker l(_propertyLock);
if (!_flags.hasCustomDrawingPriority)
return ASDefaultDrawingPriority;
else
return [objc_getAssociatedObject(self, ASDisplayNodeDrawingPriorityKey) unsignedIntegerValue];
}
- (BOOL)isInHierarchy
{
ASDisplayNodeAssertThreadAffinity(self);

View File

@ -29,6 +29,8 @@ typedef NS_ENUM(NSUInteger, ASAsyncTransactionState) {
ASAsyncTransactionStateComplete
};
extern NSUInteger const ASDefaultTransactionPriority;
/**
@summary ASAsyncTransaction provides lightweight transaction semantics for asynchronous operations.
@ -94,6 +96,25 @@ typedef NS_ENUM(NSUInteger, ASAsyncTransactionState) {
queue:(dispatch_queue_t)queue
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion;
/**
@summary Adds a synchronous operation to the transaction. The execution block will be executed immediately.
@desc The block will be executed on the specified queue and is expected to complete synchronously. The async
transaction will wait for all operations to execute on their appropriate queues, so the blocks may still be executing
async if they are running on a concurrent queue, even though the work for this block is synchronous.
@param block The execution block that will be executed on a background queue. This is where the expensive work goes.
@param priority Execution priority; Tasks with higher priority will be executed sooner
@param queue The dispatch queue on which to execute the block.
@param completion The completion block that will be executed with the output of the execution block when all of the
operations in the transaction are completed. Executed and released on callbackQueue.
*/
- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block
priority:(NSUInteger)priority
queue:(dispatch_queue_t)queue
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion;
/**
@summary Adds an async operation to the transaction. The execution block will be executed immediately.
@ -112,6 +133,27 @@ typedef NS_ENUM(NSUInteger, ASAsyncTransactionState) {
queue:(dispatch_queue_t)queue
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion;
/**
@summary Adds an async operation to the transaction. The execution block will be executed immediately.
@desc The block will be executed on the specified queue and is expected to complete asynchronously. The block will be
supplied with a completion block that can be executed once its async operation is completed. This is useful for
network downloads and other operations that have an async API.
WARNING: Consumers MUST call the completeOperationBlock passed into the work block, or objects will be leaked!
@param block The execution block that will be executed on a background queue. This is where the expensive work goes.
@param priority Execution priority; Tasks with higher priority will be executed sooner
@param queue The dispatch queue on which to execute the block.
@param completion The completion block that will be executed with the output of the execution block when all of the
operations in the transaction are completed. Executed and released on callbackQueue.
*/
- (void)addAsyncOperationWithBlock:(asyncdisplaykit_async_transaction_async_operation_block_t)block
priority:(NSUInteger)priority
queue:(dispatch_queue_t)queue
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion;
/**
@summary Adds a block to run on the completion of the async transaction.

View File

@ -13,6 +13,8 @@
#import <list>
#import <map>
NSUInteger const ASDefaultTransactionPriority = 128;
@interface ASDisplayNodeAsyncTransactionOperation : NSObject
- (id)initWithOperationCompletionBlock:(asyncdisplaykit_async_transaction_operation_completion_block_t)operationCompletionBlock;
@property (nonatomic, copy) asyncdisplaykit_async_transaction_operation_completion_block_t operationCompletionBlock;
@ -63,7 +65,7 @@ public:
virtual void release() = 0;
// schedule block on given queue
virtual void schedule(dispatch_queue_t queue, dispatch_block_t block) = 0;
virtual void schedule(NSUInteger priority, dispatch_queue_t queue, dispatch_block_t block) = 0;
// dispatch block on given queue when all previously scheduled blocks finished executing
virtual void notify(dispatch_queue_t queue, dispatch_block_t block) = 0;
@ -103,7 +105,7 @@ private:
}
virtual void release();
virtual void schedule(dispatch_queue_t queue, dispatch_block_t block);
virtual void schedule(NSUInteger priority, dispatch_queue_t queue, dispatch_block_t block);
virtual void notify(dispatch_queue_t queue, dispatch_block_t block);
virtual void enter();
virtual void leave();
@ -120,12 +122,21 @@ private:
{
dispatch_block_t _block;
GroupImpl *_group;
NSUInteger _priority;
};
struct DispatchEntry // entry for each
struct DispatchEntry // entry for each dispatch queue
{
std::list<Operation> _operations;
typedef std::list<Operation> OperationQueue;
typedef std::list<OperationQueue::iterator> OperationIteratorList; // each item points to operation queue
typedef std::map<NSUInteger, OperationIteratorList> OperationPriorityMap; // sorted by priority
OperationQueue _operationQueue;
OperationPriorityMap _operationPriorityMap;
int _threadCount;
Operation popNextOperation(bool respectPriority); // assumes locked mutex
void pushOperation(Operation operation); // assumes locked mutex
};
std::map<dispatch_queue_t, DispatchEntry> _entries;
@ -149,7 +160,44 @@ void ASAsyncTransactionQueue::GroupImpl::release()
}
}
void ASAsyncTransactionQueue::GroupImpl::schedule(dispatch_queue_t queue, dispatch_block_t block)
ASAsyncTransactionQueue::Operation ASAsyncTransactionQueue::DispatchEntry::popNextOperation(bool respectPriority)
{
NSCAssert(!_operationQueue.empty() && !_operationPriorityMap.empty(), @"No scheduled operations available");
OperationQueue::iterator queueIterator;
OperationPriorityMap::iterator mapIterator;
if (respectPriority) {
mapIterator = --_operationPriorityMap.end(); // highest priority "bucket"
queueIterator = *mapIterator->second.begin();
} else {
queueIterator = _operationQueue.begin();
mapIterator = _operationPriorityMap.find(queueIterator->_priority);
}
// no matter what, first item in "bucket" must match item in queue
NSCAssert(mapIterator->second.front() == queueIterator, @"Queue inconsistency");
Operation res = *queueIterator;
_operationQueue.erase(queueIterator);
mapIterator->second.pop_front();
if (mapIterator->second.empty()) {
_operationPriorityMap.erase(mapIterator);
}
return res;
}
void ASAsyncTransactionQueue::DispatchEntry::pushOperation(ASAsyncTransactionQueue::Operation operation)
{
_operationQueue.push_back(operation);
OperationIteratorList &list = _operationPriorityMap[operation._priority];
list.push_back(--_operationQueue.end());
}
void ASAsyncTransactionQueue::GroupImpl::schedule(NSUInteger priority, dispatch_queue_t queue, dispatch_block_t block)
{
ASAsyncTransactionQueue &q = _queue;
ASDN::MutexLocker locker(q._mutex);
@ -159,7 +207,8 @@ void ASAsyncTransactionQueue::GroupImpl::schedule(dispatch_queue_t queue, dispat
Operation operation;
operation._block = block;
operation._group = this;
entry._operations.push_back(operation);
operation._priority = priority;
entry.pushOperation(operation);
++_pendingOperations; // enter group
@ -171,15 +220,16 @@ void ASAsyncTransactionQueue::GroupImpl::schedule(dispatch_queue_t queue, dispat
if (entry._threadCount < maxThreads) { // we need to spawn another thread
// first thread will take operations in queue order (regardless of priority), other threads will respect priority
bool respectPriority = entry._threadCount > 0;
++entry._threadCount;
dispatch_async(queue, ^{
ASDN::MutexLocker lock(q._mutex);
// go until there are no more pending operations
while (!entry._operations.empty()) {
Operation operation = entry._operations.front();
entry._operations.pop_front();
while (!entry._operationQueue.empty()) {
Operation operation = entry.popNextOperation(respectPriority);
{
ASDN::MutexUnlocker unlock(q._mutex);
if (operation._block) {
@ -192,7 +242,7 @@ void ASAsyncTransactionQueue::GroupImpl::schedule(dispatch_queue_t queue, dispat
--entry._threadCount;
if (entry._threadCount == 0) {
NSCAssert(entry._operations.empty(), @"No working threads but operations are still scheduled"); // this shouldn't happen
NSCAssert(entry._operationQueue.empty() || entry._operationPriorityMap.empty(), @"No working threads but operations are still scheduled"); // this shouldn't happen
q._entries.erase(queue);
}
});
@ -295,6 +345,17 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
- (void)addAsyncOperationWithBlock:(asyncdisplaykit_async_transaction_async_operation_block_t)block
queue:(dispatch_queue_t)queue
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion
{
[self addAsyncOperationWithBlock:block
priority:ASDefaultTransactionPriority
queue:queue
completion:completion];
}
- (void)addAsyncOperationWithBlock:(asyncdisplaykit_async_transaction_async_operation_block_t)block
priority:(NSUInteger)priority
queue:(dispatch_queue_t)queue
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions");
@ -303,7 +364,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion];
[_operations addObject:operation];
_group->schedule(queue, ^{
_group->schedule(priority, queue, ^{
@autoreleasepool {
if (_state != ASAsyncTransactionStateCanceled) {
_group->enter();
@ -319,6 +380,17 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block
queue:(dispatch_queue_t)queue
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion
{
[self addOperationWithBlock:block
priority:ASDefaultTransactionPriority
queue:queue
completion:completion];
}
- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block
priority:(NSUInteger)priority
queue:(dispatch_queue_t)queue
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions");
@ -327,7 +399,7 @@ ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion];
[_operations addObject:operation];
_group->schedule(queue, ^{
_group->schedule(priority, queue, ^{
@autoreleasepool {
if (_state != ASAsyncTransactionStateCanceled) {
operation.value = block();

View File

@ -358,7 +358,7 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync,
// Adding this displayBlock operation to the transaction will start it IMMEDIATELY.
// The only function of the transaction commit is to gate the calling of the completionBlock.
[transaction addOperationWithBlock:displayBlock queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
[transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
} else {
UIImage *contents = (UIImage *)displayBlock();
completionBlock(contents, NO);

View File

@ -84,6 +84,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
unsigned shouldRasterizeDescendants:1;
unsigned shouldBypassEnsureDisplay:1;
unsigned displaySuspended:1;
unsigned hasCustomDrawingPriority:1;
// whether custom drawing is enabled
unsigned implementsDrawRect:1;