Skip to content

Commit

Permalink
Implement faster props update optimization in CKRenderComponent
Browse files Browse the repository at this point in the history
Summary:
- Added new method to `CKRenderComponentProtocol`: `isEqualToComponent:`
- Added new config `enableFasterPropsUpdate`
- When there is a `PropsUpdate`, or a `StateUpdate` on a parent component. If the `enableFasterPropsUpdate` is on, the infra will call `isEqualToComponent:` with the new generated component. If the it returns `YES`, the infra will reuse the previous component and its component tree (`CKTreeNode`).

Reviewed By: alickbass

Differential Revision: D9216877

fbshipit-source-id: 276719d7577f991afefecb014dd746c71f1e99f0
  • Loading branch information
kfirapps authored and facebook-github-bot committed Aug 14, 2018
1 parent 4e6bdf2 commit 6dbf56a
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 29 deletions.
2 changes: 2 additions & 0 deletions ComponentKit/Core/CKBuildComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ struct CKBuildComponentConfig {
BOOL buildLeafNodes = YES;
// Enable faster state updates optimization for render components.
BOOL enableFasterStateUpdates = NO;
// Enable faster props updates optimization for render components.
BOOL enableFasterPropsUpdates = NO;
};

/**
Expand Down
82 changes: 68 additions & 14 deletions ComponentKit/Core/CKRenderComponent.mm
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,40 @@ - (void)buildComponentTree:(id<CKTreeNodeWithChildrenProtocol>)parent
scopeRoot:params.scopeRoot
stateUpdates:params.stateUpdates];

// Faster state optimization.
if (config.enableFasterStateUpdates && !hasDirtyParent && previousParent && params.buildTrigger == BuildTrigger::StateUpdate) {
auto const dirtyNodeId = params.treeNodeDirtyIds.find(node.nodeIdentifier);
// Check if the tree node is not dirty (not in a branch of a state update).
if (dirtyNodeId == params.treeNodeDirtyIds.end()) {
auto const componentKey = node.componentKey;
auto const previousChild = [previousParent childForComponentKey:componentKey];
// Link the previous child to the new parent.
[parent setChild:previousChild forComponentKey:componentKey];
// Link the previous child component to the the new component.
_childComponent = [(CKRenderTreeNodeWithChild *)previousChild child].component;
return;
// Faster state/props optimizations require previous parent.
if (previousParent) {
if (params.buildTrigger == BuildTrigger::StateUpdate) {
// During state update, we have two possible optimizations:
// 1. Faster state update
// 2. Faster props update (when the parent is dirty, we handle state update as props update).
if (config.enableFasterStateUpdates || config.enableFasterPropsUpdates) {
// Check if the tree node is not dirty (not in a branch of a state update).
auto const dirtyNodeId = params.treeNodeDirtyIds.find(node.nodeIdentifier);
if (dirtyNodeId == params.treeNodeDirtyIds.end()) {
// If the component is not dirty and it doesn't have a dirty parent - we can reuse it.
if (!hasDirtyParent) {
if (config.enableFasterStateUpdates) {
// Faster state update optimizations.
reusePreviousComponent(self, node, parent, previousParent);
return;
}
} // If the component is not dirty, but its parent is dirty - we handle it as props update.
else if (config.enableFasterPropsUpdates &&
reusePreviousComponentIfComponentsAreEqual(self, node, parent, previousParent)) {
return;
}
}
else { // If the component is dirty, we mark it with `hasDirtyParent` param for its children.
hasDirtyParent = YES;
}
}
}
else { // Otherwise, update the `hasDirtyParent` param for its children.
hasDirtyParent = YES;
else if (params.buildTrigger == BuildTrigger::PropsUpdate) {
// Faster props update optimizations.
if (config.enableFasterPropsUpdates &&
reusePreviousComponentIfComponentsAreEqual(self, node, parent, previousParent)) {
return;
}
}
}

Expand All @@ -93,6 +112,36 @@ - (CKComponentLayout)computeLayoutThatFits:(CKSizeRange)constrainedSize
return {self, l.size, {{{0,0}, l}}};
}

// Reuse the previous component generation and its component tree.
static void reusePreviousComponent(CKRenderComponent *component,
CKRenderTreeNodeWithChild *node,
id<CKTreeNodeWithChildrenProtocol> parent,
id<CKTreeNodeWithChildrenProtocol> previousParent) {
auto const componentKey = node.componentKey;
auto const previousChild = [previousParent childForComponentKey:componentKey];
// Link the previous child to the new parent.
[parent setChild:previousChild forComponentKey:componentKey];
// Link the previous child component to the the new component.
component->_childComponent = [(CKRenderTreeNodeWithChild *)previousChild child].component;
}

// Check if isEqualToComponent returns `YES`; if it does, reuse the previous component generation and its component tree.
static BOOL reusePreviousComponentIfComponentsAreEqual(CKRenderComponent *component,
CKRenderTreeNodeWithChild *node,
id<CKTreeNodeWithChildrenProtocol> parent,
id<CKTreeNodeWithChildrenProtocol> previousParent) {
auto const componentKey = node.componentKey;
auto const previousChild = [previousParent childForComponentKey:componentKey];
if ([component isEqualToComponent:(id<CKRenderComponentProtocol>)previousChild.component]) {
// Link the previous child to the new parent.
[parent setChild:previousChild forComponentKey:componentKey];
// Link the previous child component to the the new component.
component->_childComponent = [(CKRenderTreeNodeWithChild *)previousChild child].component;
return YES;
}
return NO;
}

#pragma mark - CKRenderComponentProtocol

+ (id)initialStateWithComponent:(id<CKRenderComponentProtocol>)component
Expand All @@ -105,4 +154,9 @@ + (id)initialState
return [CKTreeNodeEmptyState emptyState];
}

- (BOOL)isEqualToComponent:(id<CKRenderComponentProtocol>)component
{
return NO;
}

@end
5 changes: 5 additions & 0 deletions ComponentKit/Core/CKRenderWithChildrenComponent.mm
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,9 @@ + (id)initialState
return [CKTreeNodeEmptyState emptyState];
}

- (BOOL)isEqualToComponent:(id<CKRenderComponentProtocol>)component
{
return NO;
}

@end
5 changes: 5 additions & 0 deletions ComponentKit/Core/CKRenderWithSizeSpecComponent.mm
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ + (id)initialState
return [CKTreeNodeEmptyState emptyState];
}

- (BOOL)isEqualToComponent:(id<CKRenderComponentProtocol>)component
{
return NO;
}

#pragma mark - Render layout checker

#if CK_ASSERTIONS_ENABLED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,13 @@
*/
+ (id)initialStateWithComponent:(id<CKRenderComponentProtocol>)component;

/*
Override this method in order to allow the infrastructure to reuse previous components.
You can always assume that the `component` parameter is the same type as your component.
The default value is `NO`
*/
- (BOOL)isEqualToComponent:(id<CKRenderComponentProtocol>)component;

@end
Loading

0 comments on commit 6dbf56a

Please sign in to comment.