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