Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lock up to yogaRoot during layout to avoid deadlocks. #1356

Merged
merged 3 commits into from
Mar 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 80 additions & 10 deletions Source/ASDisplayNode+Layout.mm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASLayoutElementStylePrivate.h>
#import <AsyncDisplayKit/ASLog.h>
#import <AsyncDisplayKit/ASNodeController+Beta.h>
#import <AsyncDisplayKit/ASDisplayNode+Yoga.h>

#pragma mark - ASDisplayNode (ASLayoutElement)

Expand Down Expand Up @@ -65,7 +67,7 @@ - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize

- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize
{
ASDN::MutexLocker l(__instanceLock__);
ASScopedLockSelfOrToRoot();

// If one or multiple layout transitions are in flight it still can happen that layout information is requested
// on other threads. As the pending and calculated layout to be updated in the layout transition in here just a
Expand Down Expand Up @@ -240,11 +242,11 @@ - (void)_u_setNeedsLayoutFromAbove
}
}

// TODO It would be easier to work with if we could `ASAssertUnlocked` here, but we
// cannot due to locking to root in `_u_measureNodeWithBoundsIfNecessary`.
- (void)_rootNodeDidInvalidateSize
{
ASDisplayNodeAssertThreadAffinity(self);
ASAssertUnlocked(__instanceLock__);

__instanceLock__.lock();

// We are the root node and need to re-flow the layout; at least one child needs a new size.
Expand All @@ -270,11 +272,21 @@ - (void)_rootNodeDidInvalidateSize
}
}

// TODO
// We should remove this logic, which is relatively new, and instead
// rely on the parent / host of the root node to do this size change. That's always been the
// expectation with other node containers like ASTableView, ASCollectionView, ASViewController, etc.
// E.g. in ASCellNode the _interactionDelegate is a Table or Collection that will resize in this
// case. By resizing without participating with the parent, we could get cases where our parent size
// does not match, especially if there is a size constraint that is applied at that level.
//
// In general a node should never need to set its own size, instead allowing its parent to do so -
// even in the root case. Anyhow this is a separate / pre-existing issue, but I think it could be
// causing real issues in cases of resizing nodes.
- (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size
{
ASDisplayNodeAssertThreadAffinity(self);
ASAssertUnlocked(__instanceLock__);


// The default implementation of display node changes the size of itself to the new size
CGRect oldBounds = self.bounds;
CGSize oldSize = oldBounds.size;
Expand All @@ -295,9 +307,9 @@ - (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size

- (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds
{
ASAssertUnlocked(__instanceLock__);

ASDN::MutexLocker l(__instanceLock__);
// ASAssertUnlocked(__instanceLock__);
ASScopedLockSelfOrToRoot();

// Check if we are a subnode in a layout transition.
// In this case no measurement is needed as it's part of the layout transition
if ([self _locked_isLayoutTransitionInvalid]) {
Expand Down Expand Up @@ -455,7 +467,7 @@ - (ASSizeRange)_locked_constrainedSizeForLayoutPass
- (void)_layoutSublayouts
{
ASDisplayNodeAssertThreadAffinity(self);
ASAssertUnlocked(__instanceLock__);
// ASAssertUnlocked(__instanceLock__);

ASLayout *layout;
{
Expand Down Expand Up @@ -620,7 +632,7 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize
NSUInteger newLayoutVersion = _layoutVersion;
ASLayout *newLayout;
{
ASDN::MutexLocker l(__instanceLock__);
ASScopedLockSelfOrToRoot();

ASLayoutElementContext *ctx = [[ASLayoutElementContext alloc] init];
ctx.transitionID = transitionID;
Expand Down Expand Up @@ -1009,3 +1021,61 @@ - (void)_locked_setCalculatedDisplayNodeLayout:(const ASDisplayNodeLayout &)disp
}

@end

#pragma mark -
#pragma mark - ASDisplayNode (YogaLayout)

@implementation ASDisplayNode (YogaInternal)

#pragma mark -
#pragma mark - ASDisplayNode (Yoga)

- (BOOL)locked_shouldLayoutFromYogaRoot {
#if YOGA
YGNodeRef yogaNode = _style.yogaNode;
BOOL hasYogaParent = (_yogaParent != nil);
BOOL hasYogaChildren = (_yogaChildren.count > 0);
BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren));
if (usesYoga) {
if ([self shouldHaveYogaMeasureFunc] == NO) {
return YES;
} else {
return NO;
}
} else {
return NO;
}
#else
return NO;
#endif
}

- (ASLockSet)lockToRootIfNeededForLayout {
ASLockSet lockSet = ASLockSequence(^BOOL(ASAddLockBlock addLock) {
if (!addLock(self)) {
return NO;
}
if (self.nodeController && !addLock(self.nodeController)) {
return NO;
}
#if YOGA
if (![self locked_shouldLayoutFromYogaRoot]) {
return YES;
}
ASDisplayNode *parent = _supernode;
while (parent) {
if (!addLock(parent)) {
return NO;
}
if (parent.nodeController && !addLock(parent.nodeController)) {
return NO;
}
parent = parent->_supernode;
}
#endif
return true;
});
return lockSet;
}

@end
16 changes: 16 additions & 0 deletions Source/ASDisplayNode+Yoga.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ AS_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullab
// Will walk up the Yoga tree and returns the root node
- (ASDisplayNode *)yogaRoot;

/**
* @discussion Attempts(spinning) to lock all node up to root node when yoga is enabled.
* This will lock self when yoga is not enabled;
*/
- (ASLockSet)lockToRootIfNeededForLayout;

@end


Expand All @@ -47,6 +53,11 @@ AS_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullab
- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize;
/// For internal usage only
- (void)invalidateCalculatedYogaLayout;
/**
* @discussion return true only when yoga enabled and the node is in yoga tree and the node is
* not leaf that implemented measure function.
*/
- (BOOL)locked_shouldLayoutFromYogaRoot;

@end

Expand Down Expand Up @@ -79,4 +90,9 @@ AS_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullab

NS_ASSUME_NONNULL_END

// When Yoga is enabled, there are several points where we want to lock the tree to the root but otherwise (without Yoga)
// will want to simply lock self.
#define ASScopedLockSelfOrToRoot() ASScopedLockSet lockSet = [self lockToRootIfNeededForLayout]
#else
#define ASScopedLockSelfOrToRoot() ASLockScopeSelf()
#endif
56 changes: 25 additions & 31 deletions Source/ASDisplayNode+Yoga.mm
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ - (ASDisplayNode *)yogaRoot

- (void)setYogaChildren:(NSArray *)yogaChildren
{
ASLockScope(self.yogaRoot);
ASScopedLockSelfOrToRoot();
for (ASDisplayNode *child in [_yogaChildren copy]) {
// Make sure to un-associate the YGNodeRef tree before replacing _yogaChildren
// If this becomes a performance bottleneck, it can be optimized by not doing the NSArray removals here.
Expand All @@ -66,7 +66,7 @@ - (NSArray *)yogaChildren

- (void)addYogaChild:(ASDisplayNode *)child
{
ASLockScope(self.yogaRoot);
ASScopedLockSelfOrToRoot();
[self _locked_addYogaChild:child];
}

Expand All @@ -77,7 +77,7 @@ - (void)_locked_addYogaChild:(ASDisplayNode *)child

- (void)removeYogaChild:(ASDisplayNode *)child
{
ASLockScope(self.yogaRoot);
ASScopedLockSelfOrToRoot();
[self _locked_removeYogaChild:child];
}

Expand All @@ -95,7 +95,7 @@ - (void)_locked_removeYogaChild:(ASDisplayNode *)child

- (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index
{
ASLockScope(self.yogaRoot);
ASScopedLockSelfOrToRoot();
[self _locked_insertYogaChild:child atIndex:index];
}

Expand Down Expand Up @@ -129,6 +129,7 @@ - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute

- (void)setYogaParent:(ASDisplayNode *)yogaParent
{
ASLockScopeSelf();
if (_yogaParent == yogaParent) {
return;
}
Expand Down Expand Up @@ -184,7 +185,7 @@ - (ASLayout *)layoutForYogaNode

- (void)setupYogaCalculatedLayout
{
ASLockScopeSelf();
ASScopedLockSelfOrToRoot();

YGNodeRef yogaNode = self.style.yogaNode;
uint32_t childCount = YGNodeGetChildCount(yogaNode);
Expand All @@ -194,7 +195,7 @@ - (void)setupYogaCalculatedLayout

ASLayout *rawSublayouts[childCount];
int i = 0;
for (ASDisplayNode *subnode in self.yogaChildren) {
for (ASDisplayNode *subnode in _yogaChildren) {
rawSublayouts[i++] = [subnode layoutForYogaNode];
}
const auto sublayouts = [NSArray<ASLayout *> arrayByTransferring:rawSublayouts count:childCount];
Expand Down Expand Up @@ -251,10 +252,11 @@ - (void)setupYogaCalculatedLayout

- (BOOL)shouldHaveYogaMeasureFunc
{
ASLockScopeSelf();
// Size calculation via calculateSizeThatFits: or layoutSpecThatFits:
// For these nodes, we assume they may need custom Baseline calculation too.
// This will be used for ASTextNode, as well as any other node that has no Yoga children
BOOL isLeafNode = (self.yogaChildren.count == 0);
BOOL isLeafNode = (_yogaChildren.count == 0);
BOOL definesCustomLayout = [self implementsLayoutMethod];
return (isLeafNode && definesCustomLayout);
}
Expand Down Expand Up @@ -296,31 +298,24 @@ - (ASLayout *)calculateLayoutYoga:(ASSizeRange)constrainedSize
// - This node is a Yoga tree root: it has no yogaParent, but has yogaChildren.
// - This node is a Yoga tree node: it has both a yogaParent and yogaChildren.
// - This node is a Yoga tree leaf: it has a yogaParent, but no yogaChidlren.
YGNodeRef yogaNode = _style.yogaNode;
BOOL hasYogaParent = (_yogaParent != nil);
BOOL hasYogaChildren = (_yogaChildren.count > 0);
BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren));
if (usesYoga) {
// This node has some connection to a Yoga tree.
if ([self shouldHaveYogaMeasureFunc] == NO) {
// If we're a yoga root, tree node, or leaf with no measure func (e.g. spacer), then
// initiate a new Yoga calculation pass from root.

as_activity_create_for_scope("Yoga layout calculation");
if (self.yogaLayoutInProgress == NO) {
ASYogaLog("Calculating yoga layout from root %@, %@", self, NSStringFromASSizeRange(constrainedSize));
l.unlock();
[self calculateLayoutFromYogaRoot:constrainedSize];
l.lock();
} else {
ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout);
}
ASDisplayNodeAssert(_yogaCalculatedLayout, @"Yoga node should have a non-nil layout at this stage: %@", self);
return _yogaCalculatedLayout;
if ([self locked_shouldLayoutFromYogaRoot]) {
// If we're a yoga root, tree node, or leaf with no measure func (e.g. spacer), then
// initiate a new Yoga calculation pass from root.
as_activity_create_for_scope("Yoga layout calculation");
if (self.yogaLayoutInProgress == NO) {
ASYogaLog("Calculating yoga layout from root %@, %@", self,
NSStringFromASSizeRange(constrainedSize));
[self calculateLayoutFromYogaRoot:constrainedSize];
} else {
// If we're a yoga leaf node with custom measurement function, proceed with normal layout so layoutSpecs can run (e.g. ASButtonNode).
ASYogaLog("PROCEEDING past Yoga check to calculate ASLayout for: %@", self);
ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout);
}
ASDisplayNodeAssert(_yogaCalculatedLayout,
@"Yoga node should have a non-nil layout at this stage: %@", self);
return _yogaCalculatedLayout;
} else {
// If we're a yoga leaf node with custom measurement function, proceed with normal layout so
// layoutSpecs can run (e.g. ASButtonNode).
ASYogaLog("PROCEEDING past Yoga check to calculate ASLayout for: %@", self);
}

// Delegate to layout spec layout for nodes that do not support Yoga
Expand All @@ -345,7 +340,6 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize
}
}];

ASLockScopeSelf();

// Prepare all children for the layout pass with the current Yoga tree configuration.
ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode *_Nonnull node) {
Expand Down
8 changes: 3 additions & 5 deletions Source/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1046,7 +1046,7 @@ - (void)invalidateCalculatedLayout
- (void)__layout
{
ASDisplayNodeAssertThreadAffinity(self);
ASAssertUnlocked(__instanceLock__);
// ASAssertUnlocked(__instanceLock__);

BOOL loaded = NO;
{
Expand Down Expand Up @@ -1096,7 +1096,7 @@ - (void)__layout
- (void)_layoutDidFinish
{
ASDisplayNodeAssertMainThread();
ASAssertUnlocked(__instanceLock__);
// ASAssertUnlocked(__instanceLock__);
ASDisplayNodeAssertTrue(self.isNodeLoaded);
[self layoutDidFinish];
}
Expand Down Expand Up @@ -1169,7 +1169,7 @@ - (void)layout
{
// Hook for subclasses
ASDisplayNodeAssertMainThread();
ASAssertUnlocked(__instanceLock__);
// ASAssertUnlocked(__instanceLock__);
ASDisplayNodeAssertTrue(self.isNodeLoaded);
[self enumerateInterfaceStateDelegates:^(id<ASInterfaceStateDelegate> del) {
[del nodeDidLayout];
Expand Down Expand Up @@ -2684,7 +2684,6 @@ - (void)__enterHierarchy
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"Should not cause recursive __enterHierarchy");
ASAssertUnlocked(__instanceLock__);
ASDisplayNodeLogEvent(self, @"enterHierarchy");

// Profiling has shown that locking this method is beneficial, so each of the property accesses don't have to lock and unlock.
Expand Down Expand Up @@ -2733,7 +2732,6 @@ - (void)__exitHierarchy
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"Should not cause recursive __exitHierarchy");
ASAssertUnlocked(__instanceLock__);
ASDisplayNodeLogEvent(self, @"exitHierarchy");

// Profiling has shown that locking this method is beneficial, so each of the property accesses don't have to lock and unlock.
Expand Down
8 changes: 7 additions & 1 deletion Source/ASNodeController+Beta.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

/* ASNodeController is currently beta and open to change in the future */
@interface ASNodeController<__covariant DisplayNodeType : ASDisplayNode *>
: NSObject <ASInterfaceStateDelegate, NSLocking>
: NSObject <ASInterfaceStateDelegate, ASLocking>

@property (nonatomic, strong /* may be weak! */) DisplayNodeType node;

Expand Down Expand Up @@ -44,6 +44,12 @@

- (void)hierarchyDisplayDidFinish ASDISPLAYNODE_REQUIRES_SUPER;

/**
* @discussion Attempts (via ASLockSequence, a backing-off spinlock similar to
* std::lock()) to lock both the node and its ASNodeController, if one exists.
*/
- (ASLockSet)lockPair;

@end

@interface ASDisplayNode (ASNodeController)
Expand Down
Loading