Skip to content

Commit

Permalink
Remove background deallocation helper code (facebookarchive#1890)
Browse files Browse the repository at this point in the history
* Remove background deallocation helper code

Last use removed in Texture with facebookarchive#1840, now PINS no longer uses it either. Less OOMs is so nice.

* remove methods from docs
  • Loading branch information
Greg Bolsinga authored Jul 30, 2020
1 parent f56a4dd commit 9ef97f4
Show file tree
Hide file tree
Showing 8 changed files with 1 addition and 114 deletions.
13 changes: 1 addition & 12 deletions Source/ASRunLoopQueue.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ AS_SUBCLASSING_RESTRICTED
*
* @discussion You may pass @c nil for the handler if you simply want the objects to
* be retained at enqueue time, and released during the run loop step. This is useful
* for creating a "main deallocation queue", as @c ASDeallocQueue creates its own
* worker thread with its own run loop.
* for creating a "main deallocation queue".
*/
- (instancetype)initWithRunLoop:(CFRunLoopRef)runloop
retainObjects:(BOOL)retainsObjects
Expand Down Expand Up @@ -78,14 +77,4 @@ NS_INLINE ASCATransactionQueue *ASCATransactionQueueGet(void) {
return _ASSharedCATransactionQueue;
}

@interface ASDeallocQueue : NSObject

+ (ASDeallocQueue *)sharedDeallocationQueue NS_RETURNS_RETAINED;

- (void)drain;

- (void)releaseObjectInBackground:(id __strong _Nullable * _Nonnull)objectPtr;

@end

NS_ASSUME_NONNULL_END
61 changes: 0 additions & 61 deletions Source/ASRunLoopQueue.mm
Original file line number Diff line number Diff line change
Expand Up @@ -27,67 +27,6 @@ static void runLoopSourceCallback(void *info) {
#endif
}

#pragma mark - ASDeallocQueue

@implementation ASDeallocQueue {
std::vector<CFTypeRef> _queue;
AS::Mutex _lock;
}

+ (ASDeallocQueue *)sharedDeallocationQueue NS_RETURNS_RETAINED
{
static ASDeallocQueue *deallocQueue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
deallocQueue = [[ASDeallocQueue alloc] init];
});
return deallocQueue;
}

- (void)dealloc
{
ASDisplayNodeFailAssert(@"Singleton should not dealloc.");
}

- (void)releaseObjectInBackground:(id _Nullable __strong *)objectPtr
{
NSParameterAssert(objectPtr != NULL);

// Cast to CFType so we can manipulate retain count manually.
const auto cfPtr = (CFTypeRef *)(void *)objectPtr;
if (!cfPtr || !*cfPtr) {
return;
}

_lock.lock();
const auto isFirstEntry = _queue.empty();
// Push the pointer into our queue and clear their pointer.
// This "steals" the +1 from ARC and nils their pointer so they can't
// access or release the object.
_queue.push_back(*cfPtr);
*cfPtr = NULL;
_lock.unlock();

if (isFirstEntry) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.100 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[self drain];
});
}
}

- (void)drain
{
_lock.lock();
const auto q = std::move(_queue);
_lock.unlock();
for (CFTypeRef ref : q) {
// NOTE: Could check that retain count is 1 and retry later if not.
CFRelease(ref);
}
}

@end

@implementation ASAbstractRunLoopQueue

- (instancetype)init
Expand Down
3 changes: 0 additions & 3 deletions Source/Private/ASInternalHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ ASDK_EXTERN void ASPerformBlockOnMainThread(void (^block)(void));
/// Dispatches the given block to a background queue with priority of DISPATCH_QUEUE_PRIORITY_DEFAULT if not already run on a background queue
ASDK_EXTERN void ASPerformBlockOnBackgroundThread(void (^block)(void)); // DISPATCH_QUEUE_PRIORITY_DEFAULT

/// For deallocation of objects on a background thread without GCD overhead / thread explosion
ASDK_EXTERN void ASPerformBackgroundDeallocation(id __strong _Nullable * _Nonnull object);

ASDK_EXTERN CGFloat ASScreenScale(void);

ASDK_EXTERN CGSize ASFloorSizeValues(CGSize s);
Expand Down
5 changes: 0 additions & 5 deletions Source/Private/ASInternalHelpers.mm
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,6 @@ void ASPerformBlockOnBackgroundThread(void (^block)(void))
}
}

void ASPerformBackgroundDeallocation(id __strong _Nullable * _Nonnull object)
{
[[ASDeallocQueue sharedDeallocationQueue] releaseObjectInBackground:object];
}

Class _Nullable ASGetClassFromType(const char * _Nullable type)
{
// Class types all start with @"
Expand Down
3 changes: 0 additions & 3 deletions Tests/Common/ASTestCase.mm
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,6 @@ - (void)invokeTest
@autoreleasepool {
[super invokeTest];
}

// Now that the autorelease pool is drained, drain the dealloc queue also.
[[ASDeallocQueue sharedDeallocationQueue] drain];
}

+ (ASTestCase *)currentTestCase
Expand Down
4 changes: 0 additions & 4 deletions docs/_docs/development/how-to-debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,6 @@ Here is the collection view's `dealloc`
[self setAsyncDelegate:nil];
[self setAsyncDataSource:nil];
}

// Data controller & range controller may own a ton of nodes, let's deallocate those off-main.
ASPerformBackgroundDeallocation(&_dataController);
ASPerformBackgroundDeallocation(&_rangeController);
}
```

Expand Down
6 changes: 0 additions & 6 deletions docs/_docs/development/node-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ For more details, look into ASCollectionLayout, ASCollectionGalleryLayoutDelegat

As mentioned above, since ASCellNodes are not meant to be reused, they have a longer lifecycle compared to the view or layer that they encapsulate or their corresponding UICollectionViewCell or UITableViewCell. ASCellNodes are deallocated when they are no longer used and removed from the container node. This can occur after a batch update that includes a reload data or deletion, or after the container node is no longer used and thus released.

For the latter case in which the container node is no longer used and thus released, their cell nodes are not released immediately. That is because collection and table nodes might hold a large number of cell nodes and releasing all of them (and their subnodes, more on this later) at the same time can cause a noticeable delay. To avoid that, ASCollectionNode and ASTableNode release their instance variables, most noticeably an ASDataController instance, on a background thread using a helper called ASDeallocQueue. Since the ASDataController instance is the true owner of all cell nodes -- it has a strong reference to all of them --, all of those cell nodes are deallocated off the main thread as well. As a result, you can expect a delay from which a collection or table view is released until all the cell nodes are fully released and their memory reclaimed. It's important to remember this when debugging memory leaks: objects referenced by the data controller may take a bit to show as dealloced by Instruments.

## ASDeallocQueue

As mentioned above, ASDeallocQueue helps to defer the deallocation of objects given to it by increasing the reference count of each object -- essentially retaining them and acting as their sole owner -- and then release them later on a background thread.

# Nodes that are not managed by containers

These are nodes that are often directly created by client code, such as direct and indirect subnodes of cell nodes. When a node is added to a parent node, the parent node retains it until it's removed from the parent node, or until the parent node is deallocated. As a result, if the subnode is not retained by client code in any other way or if it's not removed from the parent node, the subnode's lifecycle is tied to the parent node's lifecycle. In addition, since nodes often live in a hierarchy, the entire node hierarchy has the same lifecycle as the root node's. Lastly, if the root node is managed by a node container -- directly in the case of ASDKViewController and the like, or indirectly as a cell node of a collection or table node --, then the entire node hierarchy is managed by the node container.
Expand Down
20 changes: 0 additions & 20 deletions docs/_docs/development/threading.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,25 +250,6 @@ Since this lock is also shared, it will prevent other routines from entering unt

__Method 2__

An alternative method is to manage the duration of the lock hold manually rather than using the runtime and scope. You must remember to unlock.

```
@implementation ASDeallocQueue {
ASDN::Mutex _lock;
}
- (void)releaseObjectInBackground:(id _Nullable __strong *)objectPtr
{
NSParameterAssert(objectPtr != NULL); // do I actually need to lock ?
// other conditions or non-shared tasks
_lock.lock();
sharedObject.modify(newData);
_lock.unlock();
}
```

__Method 3__

`ASThread` provides `ASLockScopeSelf()`. This is a convenience over `ASLockScopeUnowned(<NSLocking>)`. This will unlock itself once the scope in which the lock was created is released. Only use this when you are confident that the lock should remain until scope is complete. You can only have one lock defined for `self`, thus it will block all other branches.

```
Expand Down Expand Up @@ -311,7 +292,6 @@ API | Description |
`ASDisplayNodeAssertMainThread();` | Place this at the start of the every function definition that performs work synchronously on the main thread.
`ASPerformBlockOnMainThread(block)` | If on main thread already, run block synchronously, otherwise use `dispatch_async(dispatch_get_main_queue(block))`
`ASPerformMainThreadDeallocation(&object)` | Schedule async deallocation of UIKit components
`ASPerformBackgroundDeallocation(&object)` | Schedule async deallocation of __non-UIKit__ objects
`ASPerformBlockOnBackgroundThread(block)` | Perform work on background


Expand Down

0 comments on commit 9ef97f4

Please sign in to comment.