[ASRunLoopQueue - Performance] Add ASDeallocQueue for efficient object teardown. (#2399)

* [ASRunLoopQueue - Performance] Add ASDeallocQueue for efficient object teardown.

This measurably reduces block overhead and context switching.  In the layout benchmark,
it increases ops/s while actually reducing CPU utilization.  This suggests that we are
now at a lock-bounded local maximum, at least for tri-core devices.

* [ASDeallocQueue] Update convenience helper method and adopt in ASImageNode etc.

* [ASDeallocQueue] Reimplement the queue using a timer-based runloop.

* [Debugging] Re-enable ASDisplayNode Event Log.

* [ASDeallocQueue] Final refinements, comments, code minimization.

* [ASDeallocQueue] Fix for lock release needed in early return (refactoring typo from last commit)
This commit is contained in:
appleguy
2016-10-17 12:24:11 -07:00
committed by GitHub
parent 11293d545a
commit 0a5c1f43a8
8 changed files with 155 additions and 44 deletions

View File

@@ -531,14 +531,12 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent
// Destruction of bigger images on the main thread can be expensive // Destruction of bigger images on the main thread can be expensive
// and can take some time, so we dispatch onto a bg queue to // and can take some time, so we dispatch onto a bg queue to
// actually dealloc. // actually dealloc.
__block UIImage *image = self.image; UIImage *image = self.image;
CGSize imageSize = image.size; CGSize imageSize = image.size;
BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width ||
imageSize.height > kMinReleaseImageOnBackgroundSize.height; imageSize.height > kMinReleaseImageOnBackgroundSize.height;
if (shouldReleaseImageOnBackgroundThread) { if (shouldReleaseImageOnBackgroundThread) {
ASPerformBlockOnDeallocationQueue(^{ ASPerformBackgroundDeallocation(image);
image = nil;
});
} }
self.image = nil; self.image = nil;
} }

View File

@@ -405,14 +405,12 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
// Destruction of bigger images on the main thread can be expensive // Destruction of bigger images on the main thread can be expensive
// and can take some time, so we dispatch onto a bg queue to // and can take some time, so we dispatch onto a bg queue to
// actually dealloc. // actually dealloc.
__block UIImage *image = self.image; UIImage *image = self.image;
CGSize imageSize = image.size; CGSize imageSize = image.size;
BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width ||
imageSize.height > kMinReleaseImageOnBackgroundSize.height; imageSize.height > kMinReleaseImageOnBackgroundSize.height;
if (shouldReleaseImageOnBackgroundThread) { if (shouldReleaseImageOnBackgroundThread) {
ASPerformBlockOnDeallocationQueue(^{ ASPerformBackgroundDeallocation(image);
image = nil;
});
} }
self.animatedImage = nil; self.animatedImage = nil;
self.image = _defaultImage; self.image = _defaultImage;

View File

@@ -16,11 +16,21 @@ NS_ASSUME_NONNULL_BEGIN
@interface ASRunLoopQueue<ObjectType> : NSObject @interface ASRunLoopQueue<ObjectType> : NSObject
- (instancetype)initWithRunLoop:(CFRunLoopRef)runloop andHandler:(void(^)(ObjectType dequeuedItem, BOOL isQueueDrained))handlerBlock; - (instancetype)initWithRunLoop:(CFRunLoopRef)runloop
andHandler:(void(^)(ObjectType dequeuedItem, BOOL isQueueDrained))handlerBlock;
- (void)enqueue:(ObjectType)object; - (void)enqueue:(ObjectType)object;
@property (nonatomic, assign) NSUInteger batchSize; @property (nonatomic, assign) NSUInteger batchSize; // Default == 1.
@property (nonatomic, assign) BOOL ensureExclusiveMembership; // Default == YES. Set-like behavior.
@end
@interface ASDeallocQueue : NSObject
+ (instancetype)sharedDeallocationQueue;
- (void)releaseObjectInBackground:(id)object;
@end @end

View File

@@ -12,6 +12,7 @@
#import "ASRunLoopQueue.h" #import "ASRunLoopQueue.h"
#import "ASThread.h" #import "ASThread.h"
#import "ASLog.h"
#import <cstdlib> #import <cstdlib>
#import <deque> #import <deque>
@@ -25,10 +26,118 @@ static void runLoopSourceCallback(void *info) {
#endif #endif
} }
#pragma mark - ASDeallocQueue
@implementation ASDeallocQueue {
NSThread *_thread;
NSCondition *_condition;
std::deque<id> _queue;
ASDN::RecursiveMutex _queueLock;
}
+ (instancetype)sharedDeallocationQueue
{
static ASDeallocQueue *deallocQueue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
deallocQueue = [[ASDeallocQueue alloc] init];
});
return deallocQueue;
}
- (void)releaseObjectInBackground:(id)object
{
_queueLock.lock();
_queue.push_back(object);
_queueLock.unlock();
}
- (void)threadMain
{
@autoreleasepool {
__unsafe_unretained __typeof__(self) weakSelf = self;
// 100ms timer. No resources are wasted in between, as the thread sleeps, and each check is fast.
// This time is fast enough for most use cases without excessive churn.
CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(NULL, -1, 0.1, 0, 0, ^(CFRunLoopTimerRef timer) {
#if ASRunLoopQueueLoggingEnabled
NSLog(@"ASDeallocQueue Processing: %d objects destroyed", weakSelf->_queue.size());
#endif
weakSelf->_queueLock.lock();
std::deque<id> currentQueue = weakSelf->_queue;
if (currentQueue.size() == 0) {
weakSelf->_queueLock.unlock();
return;
}
// Sometimes we release 10,000 objects at a time. Don't hold the lock while releasing.
weakSelf->_queue = std::deque<id>();
weakSelf->_queueLock.unlock();
currentQueue.clear();
});
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFRunLoopAddTimer(runloop, timer, kCFRunLoopCommonModes);
[_condition lock];
[_condition signal];
// At this moment, the thread is guaranteed to be finished starting.
[_condition unlock];
// Keep processing events until the runloop is stopped.
CFRunLoopRun();
CFRunLoopTimerInvalidate(timer);
CFRunLoopRemoveTimer(runloop, timer, kCFRunLoopCommonModes);
}
}
- (instancetype)init
{
if ((self = [super init])) {
_condition = [[NSCondition alloc] init];
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
_thread.name = @"ASDeallocQueue";
// Use condition to ensure NSThread has finished starting.
[_condition lock];
[_thread start];
[_condition wait];
[_condition unlock];
}
return self;
}
- (void)stop
{
if (!_thread) {
return;
}
[_condition lock];
[self performSelector:@selector(_stop) onThread:_thread withObject:nil waitUntilDone:NO];
[_condition wait];
[_condition unlock];
_thread = nil;
}
- (void)_stop
{
CFRunLoopStop(CFRunLoopGetCurrent());
}
- (void)dealloc
{
[self stop];
}
@end
#pragma mark - ASRunLoopQueue
@interface ASRunLoopQueue () { @interface ASRunLoopQueue () {
CFRunLoopRef _runLoop; CFRunLoopRef _runLoop;
CFRunLoopObserverRef _runLoopObserver;
CFRunLoopSourceRef _runLoopSource; CFRunLoopSourceRef _runLoopSource;
CFRunLoopObserverRef _runLoopObserver;
std::deque<id> _internalQueue; std::deque<id> _internalQueue;
ASDN::RecursiveMutex _internalQueueLock; ASDN::RecursiveMutex _internalQueueLock;
@@ -50,8 +159,13 @@ static void runLoopSourceCallback(void *info) {
_internalQueue = std::deque<id>(); _internalQueue = std::deque<id>();
_queueConsumer = [handlerBlock copy]; _queueConsumer = [handlerBlock copy];
_batchSize = 1; _batchSize = 1;
_ensureExclusiveMembership = YES;
// Self is guaranteed to outlive the observer. Without the high cost of a weak pointer,
// __unsafe_unretained allows us to avoid flagging the memory cycle detector.
__unsafe_unretained __typeof__(self) weakSelf = self;
void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
[self processQueue]; [weakSelf processQueue];
}; };
_runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, handlerBlock); _runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, handlerBlock);
CFRunLoopAddObserver(_runLoop, _runLoopObserver, kCFRunLoopCommonModes); CFRunLoopAddObserver(_runLoop, _runLoopObserver, kCFRunLoopCommonModes);
@@ -101,7 +215,7 @@ static void runLoopSourceCallback(void *info) {
- (void)processQueue - (void)processQueue
{ {
std::deque<id> itemsToProcess = std::deque<id>(); std::deque<id> itemsToProcess = std::deque<id>();
BOOL isQueueDrained = NO; BOOL isQueueDrained = NO;
{ {
ASDN::MutexLocker l(_internalQueueLock); ASDN::MutexLocker l(_internalQueueLock);
@@ -129,9 +243,9 @@ static void runLoopSourceCallback(void *info) {
unsigned long numberOfItems = itemsToProcess.size(); unsigned long numberOfItems = itemsToProcess.size();
for (int i = 0; i < numberOfItems; i++) { for (int i = 0; i < numberOfItems; i++) {
if (isQueueDrained && i == numberOfItems - 1) { if (isQueueDrained && i == numberOfItems - 1) {
self.queueConsumer(itemsToProcess[i], YES); _queueConsumer(itemsToProcess[i], YES);
} else { } else {
self.queueConsumer(itemsToProcess[i], isQueueDrained); _queueConsumer(itemsToProcess[i], isQueueDrained);
} }
} }
@@ -139,7 +253,7 @@ static void runLoopSourceCallback(void *info) {
if (!isQueueDrained) { if (!isQueueDrained) {
CFRunLoopSourceSignal(_runLoopSource); CFRunLoopSourceSignal(_runLoopSource);
CFRunLoopWakeUp(_runLoop); CFRunLoopWakeUp(_runLoop);
} }
ASProfilingSignpostEnd(0, self); ASProfilingSignpostEnd(0, self);
} }
@@ -154,10 +268,13 @@ static void runLoopSourceCallback(void *info) {
// Check if the object exists. // Check if the object exists.
BOOL foundObject = NO; BOOL foundObject = NO;
for (id currentObject : _internalQueue) {
if (currentObject == object) { if (_ensureExclusiveMembership) {
foundObject = YES; for (id currentObject : _internalQueue) {
break; if (currentObject == object) {
foundObject = YES;
break;
}
} }
} }

View File

@@ -307,12 +307,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
if (_renderer) { if (_renderer) {
// Destruction of the layout managers/containers/text storage is quite // Destruction of the layout managers/containers/text storage is quite
// expensive, and can take some time, so we dispatch onto a bg queue to // expensive, and can take some time, so we dispatch onto a bg queue to
// actually dealloc. // actually dealloc.
__block ASTextKitRenderer *renderer = _renderer; ASPerformBackgroundDeallocation(_renderer);
ASPerformBlockOnDeallocationQueue(^{
renderer = nil;
});
_renderer = nil; _renderer = nil;
} }
} }

View File

@@ -26,8 +26,8 @@ void ASPerformBlockOnMainThread(void (^block)());
/// Dispatches the given block to a background queue with priority of DISPATCH_QUEUE_PRIORITY_DEFAULT if not already run on a background queue /// Dispatches the given block to a background queue with priority of DISPATCH_QUEUE_PRIORITY_DEFAULT if not already run on a background queue
void ASPerformBlockOnBackgroundThread(void (^block)()); // DISPATCH_QUEUE_PRIORITY_DEFAULT void ASPerformBlockOnBackgroundThread(void (^block)()); // DISPATCH_QUEUE_PRIORITY_DEFAULT
/// Dispatches a block on to a serial queue that's main purpose is for deallocation of objects on a background thread /// For deallocation of objects on a background thread without GCD overhead / thread explosion
void ASPerformBlockOnDeallocationQueue(void (^block)()); void ASPerformBackgroundDeallocation(id object);
CGFloat ASScreenScale(); CGFloat ASScreenScale();

View File

@@ -9,6 +9,7 @@
// //
#import "ASInternalHelpers.h" #import "ASInternalHelpers.h"
#import "ASRunLoopQueue.h"
#import <objc/runtime.h> #import <objc/runtime.h>
@@ -78,15 +79,9 @@ void ASPerformBlockOnBackgroundThread(void (^block)())
} }
} }
void ASPerformBlockOnDeallocationQueue(void (^block)()) void ASPerformBackgroundDeallocation(id object)
{ {
static dispatch_queue_t queue; [[ASDeallocQueue sharedDeallocationQueue] releaseObjectInBackground:object];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("org.AsyncDisplayKit.deallocationQueue", DISPATCH_QUEUE_SERIAL);
});
dispatch_async(queue, block);
} }
CGFloat ASScreenScale() CGFloat ASScreenScale()

View File

@@ -17,6 +17,7 @@
#import "ASTextKitTailTruncater.h" #import "ASTextKitTailTruncater.h"
#import "ASTextKitFontSizeAdjuster.h" #import "ASTextKitFontSizeAdjuster.h"
#import "ASInternalHelpers.h" #import "ASInternalHelpers.h"
#import "ASRunLoopQueue.h"
//#define LOG(...) NSLog(__VA_ARGS__) //#define LOG(...) NSLog(__VA_ARGS__)
#define LOG(...) #define LOG(...)
@@ -127,17 +128,13 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet()
// truncater do it's job again for the new constrained size. This is necessary as after a truncation did happen // truncater do it's job again for the new constrained size. This is necessary as after a truncation did happen
// the context would use the truncated string and not the original string to truncate based on the new // the context would use the truncated string and not the original string to truncate based on the new
// constrained size // constrained size
__block ASTextKitContext *ctx = _context;
__block ASTextKitTailTruncater *tru = _truncater; ASPerformBackgroundDeallocation(_context);
__block ASTextKitFontSizeAdjuster *adj = _fontSizeAdjuster; ASPerformBackgroundDeallocation(_truncater);
ASPerformBackgroundDeallocation(_fontSizeAdjuster);
_context = nil; _context = nil;
_truncater = nil; _truncater = nil;
_fontSizeAdjuster = nil; _fontSizeAdjuster = nil;
ASPerformBlockOnDeallocationQueue(^{
ctx = nil;
tru = nil;
adj = nil;
});
} }
} }