ComponentKit/TransactionalDataSources/Common/CKDataSourceChangesetApplicator.mm (286 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 "CKDataSourceChangesetApplicator.h" #import <atomic> #import <vector> #import <ComponentKit/CKDataSourceAppliedChanges.h> #import <ComponentKit/CKDataSourceChange.h> #import <ComponentKit/CKDataSourceChangesetModification.h> #import <ComponentKit/CKDataSourceConfigurationInternal.h> #import <ComponentKit/CKDataSourceInternal.h> #import <ComponentKit/CKDataSourceListener.h> #import <ComponentKit/CKDataSourceModificationHelper.h> #import <ComponentKit/CKDataSourceQOSHelper.h> #import <ComponentKit/CKDataSourceState.h> #import <ComponentKit/CKDataSourceSplitChangesetModification.h> #import <ComponentKit/CKNonNull.h> #import <ComponentKit/CKSystraceScope.h> #import <ComponentKit/CKTraitCollectionHelper.h> static void *kQueueKey = &kQueueKey; static NSString *const kChangesetApplicatorIdUserInfoKey = @"CKDataSourceChangesetApplicator.Id"; struct CKDataSourceChangesetApplicatorPipelineItem { CKDataSourceChangeset *changeset; NSDictionary *userInfo; CKDataSourceQOS qos; BOOL hasSplitChangeset; }; @interface CKDataSourceChangesetApplicator () <CKDataSourceChangesetModificationItemGenerator, CKDataSourceListener> @end @implementation CKDataSourceChangesetApplicator { __weak CKDataSource *_dataSource; CKDataSourceState *_dataSourceState; dispatch_queue_t _queue; NSNumber *_changesetApplicatorId; std::shared_ptr<CKTreeLayoutCache> _treeLayoutCache; std::vector<CKDataSourceChangesetApplicatorPipelineItem> _pipeline; NSUInteger _pipelineId; NSMapTable<CKDataSourceChangeset *, NSMapTable<id, CKDataSourceItem *> *> *_dataSourceItemCache; CKDataSourceChangeset *_currentChangeset; CKDataSourceViewport _viewport; UITraitCollection *_traitCollection; } - (instancetype)initWithDataSource:(CKDataSource *)dataSource queue:(dispatch_queue_t)queue { static std::atomic_int32_t globalChangesetApplicatorId; if (self = [super init]) { _dataSource = dataSource; _dataSourceState = dataSource.state; _queue = queue; _changesetApplicatorId = @(++globalChangesetApplicatorId); [_dataSource addListener:self]; if (CKReadGlobalConfig().enableLayoutCaching) { _treeLayoutCache = std::make_shared<CKTreeLayoutCache>(); } RCAssertNotNil(_queue, @"A dispatch queue must be specified for changeset applicator."); RCAssert(dispatch_queue_get_specific(_queue, kQueueKey) == NULL, @"Sharing queue between changeset applicators is not allowed."); dispatch_queue_set_specific(_queue, kQueueKey, kQueueKey, NULL); _dataSourceItemCache = _createMapTable(); } return self; } - (void)dealloc { dispatch_queue_set_specific(_queue, kQueueKey, NULL, NULL); } - (void)applyChangeset:(CKDataSourceChangeset *)changeset userInfo:(NSDictionary *)userInfo qos:(CKDataSourceQOS)qos { [self applyChangeset:changeset userInfo:userInfo qos:qos hasSplitChangeset:NO]; } - (void)applyChangeset:(CKDataSourceChangeset *)changeset userInfo:(NSDictionary *)userInfo qos:(CKDataSourceQOS)qos hasSplitChangeset:(BOOL)hasSplitChangeset { if (!_isRunningOnQueue()) { auto const asyncSwitchToApply = CK::Analytics::willStartAsyncBlock(CK::Analytics::BlockName::ChangeSetApplicatorWillSwitchToApply); dispatch_async(_queue, blockUsingDataSourceQOS(^{ CKSystraceScope switchToApplyScope(asyncSwitchToApply); [self applyChangeset:changeset userInfo:userInfo qos:qos hasSplitChangeset:hasSplitChangeset]; }, qos)); return; } dispatch_async(dispatch_get_main_queue(), ^{ self->_dataSource.shouldPauseStateUpdates = YES; }); userInfo = _mergeUserInfoWithChangesetApplicatorId(userInfo, _changesetApplicatorId); BOOL shouldSplitChangeset = !hasSplitChangeset && _dataSourceState.configuration.options.splitChangesetOptions.enabled; // `_currentChangeset` is used in `buildDataSourceItemForPreviousRoot` for querying item cache for inserted items. _currentChangeset = changeset; __block CKDataSourceChange *change = nil; { id<CKDataSourceStateModifying> modification = nil; auto treeLayoutCacheCopy = _treeLayoutCache ? std::make_unique<CKTreeLayoutCache>(*_treeLayoutCache) : nullptr; if (!shouldSplitChangeset) { const auto m = [[CKDataSourceChangesetModification alloc] initWithChangeset:changeset stateListener:_dataSource userInfo:userInfo qos:qos treeLayoutCache:std::move(treeLayoutCacheCopy)]; [m setItemGenerator:self]; modification = m; } else { modification = [[CKDataSourceSplitChangesetModification alloc] initWithChangeset:changeset stateListener:_dataSource userInfo:userInfo viewport:_viewport qos:qos treeLayoutCache:std::move(treeLayoutCacheCopy)]; } CKPerformWithCurrentTraitCollection(_traitCollection, ^{ @autoreleasepool { change = [modification changeFromState:_dataSourceState]; } }); } _currentChangeset = nil; _dataSourceState = change.state; CKDataSourceChangeset *deferredChangeset = nil; if (!shouldSplitChangeset) { _pipeline.push_back({changeset, userInfo, qos, hasSplitChangeset}); } else { deferredChangeset = change.deferredChangeset; // Changeset applicator will take over deferred changeset, so nil it out in `CKDataSourceChange`. change = [[CKDataSourceChange alloc] initWithState:change.state previousState:change.previousState appliedChanges:change.appliedChanges appliedChangeset:change.appliedChangeset deferredChangeset:nil addedComponentControllers:change.addedComponentControllers invalidComponentControllers:change.invalidComponentControllers]; // Only push `appliedChangeset` to pipeline so that we can retry if // any changeset application fails. _pipeline.push_back({change.appliedChangeset, userInfo, qos, YES}); } NSUInteger pipelineId = _pipelineId; auto const willVerifyChange = CK::Analytics::willStartAsyncBlock(CK::Analytics::BlockName::ChangeSetApplicatorWillVerifyChange); dispatch_async(dispatch_get_main_queue(), ^{ CKSystraceScope willVerifyChangeScope(willVerifyChange); const auto dataSource = self->_dataSource; if (!dataSource) { // We should stop processing changesets if `dataSource` is already deallocated. return; } const auto isValid = [dataSource verifyChange:change]; const auto newState = dataSource.state; auto const willApplyChange = CK::Analytics::willStartAsyncBlock(CK::Analytics::BlockName::ChangeSetApplicatorWillApplyChange); if (CKReadGlobalConfig().enableLayoutCaching) { for (NSIndexPath *insertedIndex in [change.appliedChanges insertedIndexPaths]) { if ([newState numberOfSections] > insertedIndex.section && [newState numberOfObjectsInSection:insertedIndex.section] > insertedIndex.row) { CKDataSourceItem *insertedItem = [newState objectAtIndexPath:insertedIndex]; _treeLayoutCache->update([[insertedItem scopeRoot] globalIdentifier], insertedItem.rootLayout.cache()); } } for (NSIndexPath *updatedIndex in [change.appliedChanges finalUpdatedIndexPaths]) { if ([newState numberOfSections] > updatedIndex.section && [newState numberOfObjectsInSection:updatedIndex.section] > updatedIndex.row) { CKDataSourceItem *updatedItem = [newState objectAtIndexPath:updatedIndex]; _treeLayoutCache->update([[updatedItem scopeRoot] globalIdentifier], updatedItem.rootLayout.cache()); } } } dispatch_async(self->_queue, blockUsingDataSourceQOS(^{ CKSystraceScope willApplyChangeScope(willApplyChange); if (self->_pipelineId != pipelineId) { // We don't need to handle the result since the current pipeline was discarded. return; } if (isValid) { self->_pipeline.erase(self->_pipeline.begin()); [self->_dataSourceItemCache removeObjectForKey:changeset]; if (self->_pipeline.size() == 0) { dispatch_async(dispatch_get_main_queue(), ^{ self->_dataSource.shouldPauseStateUpdates = NO; }); } } else { [self createNewPipelineWithNewDataSourceState:newState]; } }, qos)); __unused const auto isApplied = [dataSource applyChange:change]; RCCAssert(isApplied == isValid, @"`CKDataSourceChange` is verified but not able to be applied."); }); if (shouldSplitChangeset && deferredChangeset) { // In order to guarantee the order of applied changesets, we need to apply // deferred changeset in the same runloop. [self applyChangeset:deferredChangeset userInfo:userInfo qos:qos hasSplitChangeset:YES]; } } - (void)setViewPort:(CKDataSourceViewport)viewport { dispatch_async(_queue, ^{ _viewport = viewport; }); } - (void)setTraitCollection:(UITraitCollection *)traitCollection { dispatch_async(_queue, ^{ _traitCollection = [traitCollection copy]; }); } #pragma mark - Internal static BOOL _isRunningOnQueue() { return dispatch_get_specific(kQueueKey) == kQueueKey; } static NSMapTable *_createMapTable() { return [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality valueOptions:NSPointerFunctionsStrongMemory capacity:10]; } static NSDictionary *_mergeUserInfoWithChangesetApplicatorId(NSDictionary *userInfo, NSNumber *changesetApplicatorId) { NSMutableDictionary *mutableDictionary = [userInfo mutableCopy] ?: [NSMutableDictionary new]; mutableDictionary[kChangesetApplicatorIdUserInfoKey] = changesetApplicatorId; return mutableDictionary; } - (void)createNewPipelineWithNewDataSourceState:(CKDataSourceState *)newState { RCAssert(_isRunningOnQueue(), @"Pipeline must be created on process queue."); if (![_dataSourceState.configuration isEqual:newState.configuration]) { // Discard item cache if configuraiton is updated because `sizeRange` or `context` could affect layout. _dataSourceItemCache = _createMapTable(); } _dataSourceState = newState; // A new pipeline is created and items in the existing pipeline are moved to the new one. _pipelineId++; const auto pipeline = _pipeline; _pipeline = {}; for (const auto &item : pipeline) { [self applyChangeset:item.changeset userInfo:item.userInfo qos:item.qos hasSplitChangeset:item.hasSplitChangeset]; } } #pragma mark - CKDataSourceChangesetModificationItemGenerator - (CKDataSourceItem *)buildDataSourceItemForPreviousRoot:(CK::NonNull<CKComponentScopeRoot *>)previousRoot stateUpdates:(const CKComponentStateUpdateMap &)stateUpdates sizeRange:(const CKSizeRange &)sizeRange configuration:(CKDataSourceConfiguration *)configuration model:(id)model context:(id)context itemType:(CKDataSourceChangesetModificationItemType)itemType { RCAssert(_isRunningOnQueue(), @"`CKDataSourceItem` should be generated on process queue."); if (itemType != CKDataSourceChangesetModificationItemTypeInsert) { return CKBuildDataSourceItem(previousRoot, stateUpdates, sizeRange, configuration, model, context); } auto itemCache = [_dataSourceItemCache objectForKey:_currentChangeset]; if (!itemCache) { itemCache = _createMapTable(); [_dataSourceItemCache setObject:itemCache forKey:_currentChangeset]; } auto dataSourceItem = [itemCache objectForKey:model]; if (!dataSourceItem) { dataSourceItem = CKBuildDataSourceItem(previousRoot, stateUpdates, sizeRange, configuration, model, context); [itemCache setObject:dataSourceItem forKey:model]; } return dataSourceItem; } #pragma mark - CKDataSourceListener - (void)dataSource:(CKDataSource *)dataSource didModifyPreviousState:(CKDataSourceState *)previousState withState:(CKDataSourceState *)state byApplyingChanges:(CKDataSourceAppliedChanges *)changes { // We don't need to handle new dataSource state that are coming from changeset applicator itself. if ([_changesetApplicatorId isEqual:changes.userInfo[kChangesetApplicatorIdUserInfoKey]]) { return; } dispatch_async(_queue, ^{ [self createNewPipelineWithNewDataSourceState:state]; }); } - (void)dataSource:(CKDataSource *)dataSource willApplyDeferredChangeset:(CKDataSourceChangeset *)deferredChangeset { } @end