mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
[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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user