ComponentKit/TransactionalDataSources/Common/CKDataSource.mm (533 lines of code) (raw):
/*
* 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 root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "CKDataSource.h"
#import "CKDataSourceInternal.h"
#import <ComponentKit/CKAnalyticsListener.h>
#import <ComponentKit/CKMutex.h>
#import <ComponentKit/CKRootTreeNode.h>
#import "CKComponentControllerEvents.h"
#import "CKComponentEvents.h"
#import "CKComponentControllerInternal.h"
#import "CKComponentDebugController.h"
#import "CKComponentScopeRoot.h"
#import "CKComponentSubclass.h"
#import "CKDataSourceAppliedChanges.h"
#import "CKDataSourceChange.h"
#import "CKDataSourceChangeset.h"
#import "CKDataSourceChangesetModification.h"
#import "CKDataSourceChangesetVerification.h"
#import "CKDataSourceConfiguration.h"
#import "CKDataSourceConfigurationInternal.h"
#import "CKDataSourceItem.h"
#import "CKDataSourceListenerAnnouncer.h"
#import "CKDataSourceQOSHelper.h"
#import "CKDataSourceReloadModification.h"
#import "CKDataSourceSplitChangesetModification.h"
#import "CKDataSourceStateInternal.h"
#import "CKDataSourceStateModifying.h"
#import "CKDataSourceUpdateConfigurationModification.h"
#import "CKDataSourceUpdateStateModification.h"
#import "CKSystraceScope.h"
#import "CKTraitCollectionHelper.h"
// If set to 1, CKDispatchQueueSerial uses NSThread when built with TSan.
#define CKDISPATCHQUEUESERIAL_TSAN_WORKAROUND_ENABLED 1
#if defined(__has_feature) && __has_feature(thread_sanitizer) && CKDISPATCHQUEUESERIAL_TSAN_WORKAROUND_ENABLED
// TSan (ThreadSanitizer) build.
// CKDispatchQueueSerial uses NSThread to execute submitted blocks.
// NSThread has stack of size kBackgroundThreadStackSizeInBytes so that deep
// recursive calls do not cause stack overflow.
static const NSUInteger kBackgroundThreadStackSizeInBytes = 1024 * 1024 * 2; // 2 Mb.
@implementation CKDispatchQueueSerial {
// Blocks (tasks) submitted for execution.
NSMutableArray<dispatch_block_t> *_blocks;
// Thread that is used to execute blocks.
NSThread *_thread;
// A semaphore that notifies the thread that new block is available.
dispatch_semaphore_t _sem;
// A queue that protects access to _blocks.
dispatch_queue_t _blocksQueue;
}
- (instancetype)initWithName:(const char *)name {
if (self = [super init]) {
_blocksQueue = dispatch_queue_create(name, DISPATCH_QUEUE_SERIAL);
_blocks = [NSMutableArray new];
_sem = dispatch_semaphore_create(0);
__weak __typeof(self) weakSelf = self;
if (@available(iOS 10.0, *)) {
_thread = [[NSThread alloc] initWithBlock:^{
for (;;) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// Wait for blocks.
dispatch_semaphore_wait(strongSelf->_sem, DISPATCH_TIME_FOREVER);
__block dispatch_block_t block;
dispatch_sync(strongSelf->_blocksQueue, ^{
if (_blocks.count == 0) {
return;
}
// Grab next block.
block = strongSelf->_blocks[0];
[strongSelf->_blocks removeObjectAtIndex:0];
});
if (block) {
block();
} else {
// CKDispatchQueueSerial was deallocated.
return;
}
}
}];
_thread.stackSize = kBackgroundThreadStackSizeInBytes;
[_thread start];
} else {
RCFailAssert(@"ComponentKit requires iOS 10 or higher when running under TSan.");
}
}
return self;
}
- (void)dispatchAsync:(dispatch_block_t)block {
dispatch_sync(_blocksQueue, ^{
[_blocks addObject:block];
});
dispatch_semaphore_signal(_sem);
}
- (void)dealloc {
// This will cause thread wake up and
dispatch_semaphore_signal(_sem);
}
@end
#else
// Regular build (without TSan).
// CKDispatchQueueSerial is just a wrapper around dispatch_queue_t.
@implementation CKDispatchQueueSerial {
dispatch_queue_t _queue;
}
- (instancetype)initWithName:(const char *)name {
if (self = [super init]) {
_queue = dispatch_queue_create(name, DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)dispatchAsync:(dispatch_block_t)block {
dispatch_async(_queue, block);
}
@end
#endif
@interface CKDataSourceModificationPair : NSObject
@property (nonatomic, strong, readonly) id<CKDataSourceStateModifying> modification;
@property (nonatomic, strong, readonly) CKDataSourceState *state;
- (instancetype)initWithModification:(id<CKDataSourceStateModifying>)modification
state:(CKDataSourceState *)state;
@end
@interface CKDataSource () <CKComponentDebugReflowListener>
{
CKDataSourceState *_state;
CKDataSourceListenerAnnouncer *_announcer;
CKComponentStateUpdatesMap _pendingAsynchronousStateUpdates;
CKComponentStateUpdatesMap _pendingSynchronousStateUpdates;
NSMutableArray<id<CKDataSourceStateModifying>> *_pendingAsynchronousModifications;
BOOL _processingAsynchronousModification;
BOOL _shouldPauseStateUpdates;
BOOL _isBackgroundMode;
CKDispatchQueueSerial *_workQueue;
std::shared_ptr<CKTreeLayoutCache> _treeLayoutCache;
CKDataSourceViewport _viewport;
BOOL _changesetSplittingEnabled;
UITraitCollection *_traitCollection;
}
@end
@implementation CKDataSource
- (instancetype)initWithConfiguration:(CKDataSourceConfiguration *)configuration
{
return [self initWithState:[[CKDataSourceState alloc] initWithConfiguration:configuration sections:@[]]];
}
- (instancetype)initWithState:(CKDataSourceState *)state
{
RCAssertNotNil(state, @"Initial state is required");
RCAssertNotNil(state.configuration, @"Configuration is required");
if (self = [super init]) {
const auto configuration = state.configuration;
_state = state;
_announcer = [[CKDataSourceListenerAnnouncer alloc] init];
_workQueue = [[CKDispatchQueueSerial alloc] initWithName:"org.componentkit.CKDataSource"];
_pendingAsynchronousModifications = [NSMutableArray array];
_changesetSplittingEnabled = configuration.options.splitChangesetOptions.enabled;
[CKComponentDebugController registerReflowListener:self];
if (CKReadGlobalConfig().enableLayoutCaching) {
_treeLayoutCache = std::make_shared<CKTreeLayoutCache>();
}
}
return self;
}
- (void)dealloc
{
// We want to ensure that controller invalidation is called on the main thread
// The chain of ownership is following: CKDataSourceState -> array of CKDataSourceItem-> ScopeRoot -> controllers.
// We delay desctruction of DataSourceState to guarantee that controllers are alive.
CKDataSourceState *state = _state;
void (^completion)() = ^() {
[state enumerateObjectsUsingBlock:^(CKDataSourceItem *item, NSIndexPath *, BOOL *stop) {
CKComponentScopeRootAnnounceControllerInvalidation([item scopeRoot]);
}];
};
if ([NSThread isMainThread]) {
completion();
} else {
dispatch_async(dispatch_get_main_queue(), completion);
}
}
- (CKDataSourceState *)state
{
RCAssertMainThread();
return _state;
}
- (void)applyChangeset:(CKDataSourceChangeset *)changeset
mode:(CKUpdateMode)mode
userInfo:(NSDictionary *)userInfo
{
[self applyChangeset:changeset mode:mode qos:CKDataSourceQOSDefault userInfo:userInfo];
}
- (void)applyChangeset:(CKDataSourceChangeset *)changeset
mode:(CKUpdateMode)mode
qos:(CKDataSourceQOS)qos
userInfo:(NSDictionary *)userInfo
{
RCAssertMainThread();
#if CK_ASSERTIONS_ENABLED
CKVerifyChangeset(changeset, _state, _pendingAsynchronousModifications);
#endif
id<CKDataSourceStateModifying> const modification =
[self _changesetGenerationModificationForChangeset:changeset
userInfo:userInfo
qos:qos
isDeferredChangeset:NO];
switch (mode) {
case CKUpdateModeAsynchronous:
[self _enqueueModification:modification];
break;
case CKUpdateModeSynchronous:
// We need to keep FIFO ordering of changesets, so cancel & synchronously apply any queued async modifications.
NSArray *enqueuedChangesets = [self _cancelEnqueuedModificationsOfType:[modification class]];
for (id<CKDataSourceStateModifying> pendingChangesetModification in enqueuedChangesets) {
[self _synchronouslyApplyModification:pendingChangesetModification];
}
[self _synchronouslyApplyModification:modification];
break;
}
}
- (void)updateConfiguration:(CKDataSourceConfiguration *)configuration
mode:(CKUpdateMode)mode
userInfo:(NSDictionary *)userInfo
{
RCAssertMainThread();
id<CKDataSourceStateModifying> modification =
[[CKDataSourceUpdateConfigurationModification alloc] initWithConfiguration:configuration userInfo:userInfo];
switch (mode) {
case CKUpdateModeAsynchronous:
[self _enqueueModification:modification];
break;
case CKUpdateModeSynchronous:
// Cancel all enqueued asynchronous configuration updates or they'll complete later and overwrite this one.
[self _cancelEnqueuedModificationsOfType:[modification class]];
[self _synchronouslyApplyModification:modification];
break;
}
}
- (void)reloadWithMode:(CKUpdateMode)mode
userInfo:(NSDictionary *)userInfo
{
RCAssertMainThread();
auto treeLayoutCacheCopy = _treeLayoutCache ? std::make_unique<CKTreeLayoutCache>(*_treeLayoutCache) : nullptr;
id<CKDataSourceStateModifying> modification =
[[CKDataSourceReloadModification alloc] initWithUserInfo:userInfo treeLayoutCache:std::move(treeLayoutCacheCopy)];
switch (mode) {
case CKUpdateModeAsynchronous:
[self _enqueueModification:modification];
break;
case CKUpdateModeSynchronous:
// Cancel previously enqueued reloads; we're reloading right now, so no need to subsequently reload again.
[self _cancelEnqueuedModificationsOfType:[modification class]];
[self _synchronouslyApplyModification:modification];
break;
}
}
- (BOOL)applyChange:(CKDataSourceChange *)change
{
RCAssertMainThread();
if (![self verifyChange:change]) {
return NO;
}
[self _synchronouslyApplyChange:change qos:CKDataSourceQOSDefault];
return YES;
}
- (BOOL)verifyChange:(CKDataSourceChange *)change
{
RCAssertMainThread();
// We don't check `_pendingAsynchronousModifications` here because we want pre-computed `CKDataSourceChange`
// to have higher chance to be applied. Asynchronous modifications will be re-applied anyway if they fail.
return change.previousState == _state;
}
- (void)setViewport:(CKDataSourceViewport)viewport
{
RCAssertMainThread();
if (!_changesetSplittingEnabled) {
return;
}
_viewport = viewport;
}
- (void)addListener:(id<CKDataSourceListener>)listener
{
RCAssertMainThread();
[_announcer addListener:listener];
}
- (void)removeListener:(id<CKDataSourceListener>)listener
{
RCAssertMainThread();
[_announcer removeListener:listener];
}
- (void)setShouldPauseStateUpdates:(BOOL)shouldPauseStateUpdates
{
RCAssertMainThread();
_shouldPauseStateUpdates = shouldPauseStateUpdates;
if (!_shouldPauseStateUpdates) {
[self _processStateUpdates];
}
}
- (BOOL)shouldPauseStateUpdates
{
RCAssertMainThread();
return _shouldPauseStateUpdates;
}
- (void)setIsBackgroundMode:(BOOL)isBackgroundMode
{
RCAssertMainThread();
_isBackgroundMode = isBackgroundMode;
}
- (BOOL)isBackgroundMode
{
RCAssertMainThread();
return _isBackgroundMode;
}
- (void)setTraitCollection:(UITraitCollection *)traitCollection
{
RCAssertMainThread();
_traitCollection = [traitCollection copy];
}
#pragma mark - State Listener
- (void)componentScopeHandle:(CKComponentScopeHandle *)handle
rootIdentifier:(CKComponentScopeRootIdentifier)rootIdentifier
didReceiveStateUpdate:(id (^)(id))stateUpdate
metadata:(const CKStateUpdateMetadata &)metadata
mode:(CKUpdateMode)mode
{
RCAssertMainThread();
[_state.configuration.analyticsListener didReceiveStateUpdateFromScopeHandle:handle
rootIdentifier:rootIdentifier];
if (_pendingAsynchronousStateUpdates.empty() && _pendingSynchronousStateUpdates.empty()) {
dispatch_async(dispatch_get_main_queue(), ^{
[self _processStateUpdates];
});
}
if (mode == CKUpdateModeAsynchronous) {
_pendingAsynchronousStateUpdates[rootIdentifier][handle].push_back(stateUpdate);
} else {
_pendingSynchronousStateUpdates[rootIdentifier][handle].push_back(stateUpdate);
}
}
+ (BOOL)requiresMainThreadAffinedStateUpdates
{
return YES;
}
#pragma mark - CKComponentDebugReflowListener
- (void)didReceiveReflowComponentsRequest
{
[self reloadWithMode:CKUpdateModeAsynchronous userInfo:nil];
}
- (void)didReceiveReflowComponentsRequestWithTreeNodeIdentifier:(CKTreeNodeIdentifier)treeNodeIdentifier
{
__block NSIndexPath *ip = nil;
__block id model = nil;
[_state enumerateObjectsUsingBlock:^(CKDataSourceItem *item, NSIndexPath *indexPath, BOOL *stop) {
if ([item.scopeRoot rootNode].parentForNodeIdentifier(treeNodeIdentifier) != nil) {
ip = indexPath;
model = item.model;
*stop = YES;
}
}];
if (ip != nil) {
const auto changeset = [[[CKDataSourceChangesetBuilder dataSourceChangesetWithOriginName:@"ck_data_source"] withUpdatedItems:@{ip: model}] build];
[self applyChangeset:changeset mode:CKUpdateModeSynchronous userInfo:@{}];
}
}
#pragma mark - Internal
- (void)_enqueueModification:(id<CKDataSourceStateModifying>)modification
{
RCAssertMainThread();
[_pendingAsynchronousModifications addObject:modification];
if (_pendingAsynchronousModifications.count == 1) {
[self _startAsynchronousModificationIfNeeded];
}
}
- (void)_startAsynchronousModificationIfNeeded
{
RCAssertMainThread();
id<CKDataSourceStateModifying> modification = _pendingAsynchronousModifications.firstObject;
if (!_processingAsynchronousModification && _pendingAsynchronousModifications.count > 0) {
_processingAsynchronousModification = YES;
CKDataSourceModificationPair *modificationPair =
[[CKDataSourceModificationPair alloc]
initWithModification:modification
state:_state];
const auto traitCollection = _traitCollection;
auto const asyncModification = CK::Analytics::willStartAsyncBlock(CK::Analytics::BlockName::DataSourceWillStartModification);
dispatch_block_t block = blockUsingDataSourceQOS(^{
CKSystraceScope modificationScope(asyncModification);
CKPerformWithCurrentTraitCollection(traitCollection, ^{
[self _applyModificationPair:modificationPair];
});
}, [modification qos], _isBackgroundMode);
[_workQueue dispatchAsync:block];
}
}
/** Returns the canceled matching modifications, in the order they would have been applied. */
- (NSArray *)_cancelEnqueuedModificationsOfType:(Class)modificationType
{
RCAssertMainThread();
NSIndexSet *indexes = [_pendingAsynchronousModifications indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
return [obj isKindOfClass:modificationType];
}];
NSArray *modifications = [_pendingAsynchronousModifications objectsAtIndexes:indexes];
[_pendingAsynchronousModifications removeObjectsAtIndexes:indexes];
return modifications;
}
- (void)_synchronouslyApplyModification:(id<CKDataSourceStateModifying>)modification
{
CKPerformWithCurrentTraitCollection(_traitCollection, ^{
[_announcer dataSource:self willSyncApplyModificationWithUserInfo:[modification userInfo]];
[self _synchronouslyApplyChange:[modification changeFromState:_state] qos:modification.qos];
});
}
- (void)_synchronouslyApplyChange:(CKDataSourceChange *)change qos:(CKDataSourceQOS)qos
{
RCAssertMainThread();
CKDataSourceAppliedChanges *const appliedChanges = [change appliedChanges];
CKDataSourceState *const previousState = _state;
CKDataSourceState *const newState = [change state];
_state = newState;
// Announce 'didInit'.
for (CKComponentController *componentController in change.addedComponentControllers) {
[componentController didInit];
}
for (NSIndexPath *insertedIndex in [appliedChanges insertedIndexPaths]) {
CKDataSourceItem *insertedItem = [newState objectAtIndexPath:insertedIndex];
const auto scopeRoot = [insertedItem scopeRoot];
CKComponentScopeRootAnnounceControllerInitialization(scopeRoot);
if (CKReadGlobalConfig().enableLayoutCaching) {
_treeLayoutCache->update([scopeRoot globalIdentifier], insertedItem.rootLayout.cache());
}
}
if (CKReadGlobalConfig().enableLayoutCaching) {
for (NSIndexPath *updatedIndex in [appliedChanges finalUpdatedIndexPaths]) {
CKDataSourceItem *updatedItem = [newState objectAtIndexPath:updatedIndex];
_treeLayoutCache->update([[updatedItem scopeRoot] globalIdentifier], updatedItem.rootLayout.cache());
}
}
// Announce 'invalidateController'.
for (CKComponentController *componentController in change.invalidComponentControllers) {
[componentController invalidateController];
}
for (NSIndexPath *removedIndex in [appliedChanges removedIndexPaths]) {
CKDataSourceItem *removedItem = [previousState objectAtIndexPath:removedIndex];
CKComponentScopeRootAnnounceControllerInvalidation([removedItem scopeRoot]);
}
[[appliedChanges removedSections] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *) {
[previousState enumerateObjectsInSectionAtIndex:idx usingBlock:^(CKDataSourceItem *removedItem, NSIndexPath *, BOOL *) {
CKComponentScopeRootAnnounceControllerInvalidation([removedItem scopeRoot]);
}];
}];
CKComponentUpdateComponentForComponentControllerWithIndexPaths(appliedChanges.finalUpdatedIndexPaths.allValues,
newState);
[_announcer dataSource:self didModifyPreviousState:previousState withState:newState byApplyingChanges:appliedChanges];
// Announce 'didPrepareLayoutForComponent:'.
CKComponentSendDidPrepareLayoutForComponentsWithIndexPaths([[appliedChanges finalUpdatedIndexPaths] allValues], newState);
CKComponentSendDidPrepareLayoutForComponentsWithIndexPaths([appliedChanges insertedIndexPaths], newState);
// Handle deferred changeset (if there is one)
auto const deferredChangeset = [change deferredChangeset];
if (deferredChangeset != nil) {
[_announcer dataSource:self willApplyDeferredChangeset:deferredChangeset];
id<CKDataSourceStateModifying> const modification =
[self _changesetGenerationModificationForChangeset:deferredChangeset
userInfo:[appliedChanges userInfo]
qos:qos
isDeferredChangeset:YES];
// This needs to be applied asynchronously to avoid having both the first part of the changeset
// and the deferred changeset be applied in the same runloop tick -- otherwise, the completion
// of the first update will need to wait until the deferred changeset is applied and regress
// overall performance.
//
// This is manually inserted at the front of the asynchronous modifications queue to avoid having
// existing enqueued async modifications be applied against a mismatched data source state.
[_pendingAsynchronousModifications insertObject:modification atIndex:0];
if (_pendingAsynchronousModifications.count == 1) {
[self _startAsynchronousModificationIfNeeded];
}
}
}
- (void)_processStateUpdates
{
RCAssertMainThread();
if (_shouldPauseStateUpdates) {
return;
}
CKDataSourceUpdateStateModification *const asyncStateUpdateModification = [self _consumePendingAsynchronousStateUpdates];
if (asyncStateUpdateModification != nil) {
[self _enqueueModification:asyncStateUpdateModification];
}
CKDataSourceUpdateStateModification *const syncStateUpdateModification = [self _consumePendingSynchronousStateUpdates];
if (syncStateUpdateModification != nil) {
[self _synchronouslyApplyModification:syncStateUpdateModification];
}
}
- (id<CKDataSourceStateModifying>)_consumePendingSynchronousStateUpdates
{
RCAssertMainThread();
if (_pendingSynchronousStateUpdates.empty()) {
return nil;
}
auto treeLayoutCacheCopy = _treeLayoutCache ? std::make_unique<CKTreeLayoutCache>(*_treeLayoutCache) : nullptr;
CKDataSourceUpdateStateModification *const modification =
[[CKDataSourceUpdateStateModification alloc] initWithStateUpdates:_pendingSynchronousStateUpdates treeLayoutCache:std::move(treeLayoutCacheCopy)];
_pendingSynchronousStateUpdates.clear();
return modification;
}
- (id<CKDataSourceStateModifying>)_consumePendingAsynchronousStateUpdates
{
RCAssertMainThread();
if (_pendingAsynchronousStateUpdates.empty()) {
return nil;
}
auto treeLayoutCacheCopy = _treeLayoutCache ? std::make_unique<CKTreeLayoutCache>(*_treeLayoutCache) : nullptr;
CKDataSourceUpdateStateModification *const modification =
[[CKDataSourceUpdateStateModification alloc] initWithStateUpdates:_pendingAsynchronousStateUpdates treeLayoutCache:std::move(treeLayoutCacheCopy)];
_pendingAsynchronousStateUpdates.clear();
return modification;
}
- (void)_applyModificationPair:(CKDataSourceModificationPair *)modificationPair
{
[_announcer dataSource:self willGenerateNewStateWithUserInfo:modificationPair.modification.userInfo];
CKDataSourceChange *change;
@autoreleasepool {
change = [modificationPair.modification changeFromState:modificationPair.state];
}
[_announcer dataSource:self didGenerateNewState:[change state] changes:[change appliedChanges]];
auto const asyncApplyModification = CK::Analytics::willStartAsyncBlock(CK::Analytics::BlockName::DataSourceWillApplyModification);
dispatch_async(dispatch_get_main_queue(), ^{
CKSystraceScope applyModificationScope(asyncApplyModification);
// If the first object in _pendingAsynchronousModifications is not still the modification,
// it may have been canceled; don't apply it.
if ([_pendingAsynchronousModifications firstObject] == modificationPair.modification && self->_state == modificationPair.state) {
[_pendingAsynchronousModifications removeObjectAtIndex:0];
[self _synchronouslyApplyChange:change qos:modificationPair.modification.qos];
}
_processingAsynchronousModification = NO;
[self _startAsynchronousModificationIfNeeded];
});
}
- (id<CKDataSourceStateModifying>)_changesetGenerationModificationForChangeset:(CKDataSourceChangeset *)changeset
userInfo:(NSDictionary *)userInfo
qos:(CKDataSourceQOS)qos
isDeferredChangeset:(BOOL)isDeferredChangeset
{
auto treeLayoutCacheCopy = _treeLayoutCache ? std::make_unique<CKTreeLayoutCache>(*_treeLayoutCache) : nullptr;
if (!isDeferredChangeset && _changesetSplittingEnabled) {
return
[[CKDataSourceSplitChangesetModification alloc] initWithChangeset:changeset
stateListener:self
userInfo:userInfo
viewport:_viewport
qos:qos
treeLayoutCache:std::move(treeLayoutCacheCopy)];
} else {
return
[[CKDataSourceChangesetModification alloc] initWithChangeset:changeset
stateListener:self
userInfo:userInfo
qos:qos
treeLayoutCache:std::move(treeLayoutCacheCopy)];
}
}
@end
@implementation CKDataSourceModificationPair
- (instancetype)initWithModification:(id<CKDataSourceStateModifying>)modification
state:(CKDataSourceState *)state
{
if (self = [super init]) {
_modification = modification;
_state = state;
}
return self;
}
@end