Skip to content

Conversation

@tido64
Copy link
Member

@tido64 tido64 commented Jun 7, 2020

  • I am removing an existing difference between facebook/react-native and microsoft/react-native-macos 👍
  • I am cherry-picking a change from Facebook's react-native into microsoft/react-native-macos 👍
  • I am making a fix / change for the macOS implementation of react-native
  • I am making a change required for Microsoft usage of react-native

Summary

Xcode logs warnings to console about the use of the appearance API on a non-UI thread. This fix should allow the current behaviour.

Resolves #335

Changelog

[macOS] [Fixed] - Allow -[RCTDynamicColor effectiveColor] to be called off UI thread

Test Plan

Xcode should warn about the use of the appearance API on a non-UI thread.

Microsoft Reviewers: Open in CodeFlow

@tido64 tido64 requested a review from tom-un as a code owner June 7, 2020 20:23
@markavitale
Copy link

I'm not sure what the usage would be to call effectiveColor when not on the UI thread. The call site that's trying to do this seems like the issue to be fixed, not RCTDynamicColor.

Dynamic colors by definition don't resolve to a specific color until resolved within a given UI context. So in this case, we're just caching the last context in which this color was resolved and hoping that it stays accurate. That's by no means guaranteed to be true. Different view hierarchies within the same window/application can have different appearances or the user could change the system setting at runtime, so depending on a cached version of the color seems likely to provide invalid colors.

@anandrajeswaran
Copy link

+1 to what @markavitale said. This seems like this would just silence the flaw and make such bugs more difficult to detect

Copy link

@anandrajeswaran anandrajeswaran left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There shouldn't be a use case for calling this off the UI thread and adding a cache in here just increases the odds of a subtle bug.

@tido64
Copy link
Member Author

tido64 commented Jun 7, 2020

It gets called from ShadowQueue:

* thread #2, queue = 'com.facebook.react.ShadowQueue', stop reason = breakpoint 2.1
    frame #0: 0x000000010012569c RNTester-macOS`-[RCTDynamicColor effectiveColor](self=0x00006000003db520, _cmd="effectiveColor") at RCTDynamicColor.m:75:9
    frame #1: 0x0000000100125828 RNTester-macOS`-[RCTDynamicColor colorSpaceName](self=0x00006000003db520, _cmd="colorSpaceName") at RCTDynamicColor.m:87:1
    frame #2: 0x00007fff358a6f5c AppKit`-[NSColor hash] + 22
    frame #3: 0x00007fff6911ed5c UIFoundation`attributeDictionaryHash + 181
    frame #4: 0x00007fff3a75a7ac Foundation`hashProbe + 68
    frame #5: 0x00007fff3a75a752 Foundation`-[NSConcreteHashTable getItem:] + 35
    frame #6: 0x00007fff6911e8ff UIFoundation`+[NSAttributeDictionary newWithDictionary:] + 115
    frame #7: 0x00007fff3a7c1158 Foundation`-[NSConcreteAttributedString initWithString:attributes:] + 128
    frame #8: 0x0000000100257384 RNTester-macOS`-[RCTBaseTextShadowView attributedTextWithBaseTextAttributes:](self=0x000000010162ab00, _cmd="attributedTextWithBaseTextAttributes:", baseTextAttributes=0x0000000000000000) at RCTBaseTextShadowView.m:100:11
    frame #9: 0x00000001002622d2 RNTester-macOS`-[RCTTextShadowView attributedTextWithMeasuredAttachmentsThatFitSize:](self=0x000000010162ab00, _cmd="attributedTextWithMeasuredAttachmentsThatFitSize:", size=(width = 640, height = 1.7976931348623157E+308)) at RCTTextShadowView.m:184:65
    frame #10: 0x00000001002628fd RNTester-macOS`-[RCTTextShadowView textStorageAndLayoutManagerThatFitsSize:exclusiveOwnership:](self=0x000000010162ab00, _cmd="textStorageAndLayoutManagerThatFitsSize:exclusiveOwnership:", size=(width = 640, height = 1.7976931348623157E+308), exclusiveOwnership=NO) at RCTTextShadowView.m:236:53
  * frame #11: 0x0000000100260929 RNTester-macOS`RCTTextShadowViewMeasure(node=0x000000010162ad10, width=640, widthMode=YGMeasureModeAtMost, height=NaN, heightMode=YGMeasureModeUndefined) at RCTTextShadowView.m:404:5
    frame #12: 0x0000000100387bbf RNTester-macOS`YGNode::measure(this=0x000000010162ad10, width=640, widthMode=YGMeasureModeAtMost, height=NaN, heightMode=YGMeasureModeUndefined, layoutContext=0x0000000000000000) at YGNode.cpp:153:9
    frame #13: 0x00000001003a2fef RNTester-macOS`YGNodeWithMeasureFuncSetMeasuredDimensions(node=0x000000010162ad10, availableWidth=640, availableHeight=NaN, widthMeasureMode=YGMeasureModeAtMost, heightMeasureMode=YGMeasureModeUndefined, ownerWidth=640, ownerHeight=NaN, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, reason=kMeasureChild) at Yoga.cpp:1639:39
    frame #14: 0x0000000100397fbf RNTester-macOS`YGNodelayoutImpl(node=0x000000010162ad10, availableWidth=640, availableHeight=NaN, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeAtMost, heightMeasureMode=YGMeasureModeUndefined, ownerWidth=640, ownerHeight=NaN, performLayout=false, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=8, generationCount=8, reason=kMeasureChild) at Yoga.cpp:2723:5
    frame #15: 0x000000010039756c RNTester-macOS`YGLayoutNodeInternal(node=0x000000010162ad10, availableWidth=640, availableHeight=NaN, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeAtMost, heightMeasureMode=YGMeasureModeUndefined, ownerWidth=640, ownerHeight=NaN, performLayout=false, reason=kMeasureChild, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=8, generationCount=8) at Yoga.cpp:3867:5
    frame #16: 0x00000001003a792e RNTester-macOS`YGNodeComputeFlexBasisForChild(node=0x00000001026086d0, child=0x000000010162ad10, width=640, widthMode=YGMeasureModeExactly, height=NaN, ownerWidth=640, ownerHeight=NaN, heightMode=YGMeasureModeUndefined, direction=YGDirectionLTR, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=7, generationCount=8) at Yoga.cpp:1357:5
    frame #17: 0x00000001003a3bf2 RNTester-macOS`YGNodeComputeFlexBasisForChildren(node=0x00000001026086d0, availableInnerWidth=640, availableInnerHeight=NaN, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeUndefined, direction=YGDirectionLTR, mainAxis=YGFlexDirectionColumn, config=0x0000600000c8d440, performLayout=false, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=7, generationCount=8) at Yoga.cpp:1899:7
    frame #18: 0x000000010039861f RNTester-macOS`YGNodelayoutImpl(node=0x00000001026086d0, availableWidth=640, availableHeight=NaN, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeUndefined, ownerWidth=640, ownerHeight=NaN, performLayout=false, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=7, generationCount=8, reason=kAbsMeasureChild) at Yoga.cpp:2834:31
    frame #19: 0x000000010039756c RNTester-macOS`YGLayoutNodeInternal(node=0x00000001026086d0, availableWidth=640, availableHeight=NaN, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeUndefined, ownerWidth=640, ownerHeight=NaN, performLayout=false, reason=kAbsMeasureChild, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=7, generationCount=8) at Yoga.cpp:3867:5
    frame #20: 0x00000001003a6192 RNTester-macOS`YGNodeAbsoluteLayoutChild(node=0x000000010240ae70, child=0x00000001026086d0, width=640, widthMode=YGMeasureModeExactly, height=40, direction=YGDirectionLTR, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=6, generationCount=8) at Yoga.cpp:1485:5
    frame #21: 0x000000010039b6f6 RNTester-macOS`YGNodelayoutImpl(node=0x000000010240ae70, availableWidth=640, availableHeight=40, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=40, performLayout=true, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=6, generationCount=8, reason=kStretch) at Yoga.cpp:3474:7
    frame #22: 0x000000010039756c RNTester-macOS`YGLayoutNodeInternal(node=0x000000010240ae70, availableWidth=640, availableHeight=40, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=40, performLayout=true, reason=kStretch, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=6, generationCount=8) at Yoga.cpp:3867:5
    frame #23: 0x0000000100399776 RNTester-macOS`YGNodelayoutImpl(node=0x00000001017270b0, availableWidth=640, availableHeight=40.5, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=480, performLayout=true, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=5, generationCount=8, reason=kStretch) at Yoga.cpp:3118:15
    frame #24: 0x000000010039756c RNTester-macOS`YGLayoutNodeInternal(node=0x00000001017270b0, availableWidth=640, availableHeight=40.5, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=480, performLayout=true, reason=kStretch, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=5, generationCount=8) at Yoga.cpp:3867:5
    frame #25: 0x0000000100399776 RNTester-macOS`YGNodelayoutImpl(node=0x000000010215fb60, availableWidth=640, availableHeight=480, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=480, performLayout=true, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=4, generationCount=8, reason=kStretch) at Yoga.cpp:3118:15
    frame #26: 0x000000010039756c RNTester-macOS`YGLayoutNodeInternal(node=0x000000010215fb60, availableWidth=640, availableHeight=480, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=480, performLayout=true, reason=kStretch, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=4, generationCount=8) at Yoga.cpp:3867:5
    frame #27: 0x0000000100399776 RNTester-macOS`YGNodelayoutImpl(node=0x0000000102142680, availableWidth=640, availableHeight=480, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=480, performLayout=true, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=3, generationCount=8, reason=kStretch) at Yoga.cpp:3118:15
    frame #28: 0x000000010039756c RNTester-macOS`YGLayoutNodeInternal(node=0x0000000102142680, availableWidth=640, availableHeight=480, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=480, performLayout=true, reason=kStretch, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=3, generationCount=8) at Yoga.cpp:3867:5
    frame #29: 0x0000000100399776 RNTester-macOS`YGNodelayoutImpl(node=0x00000001021466f0, availableWidth=640, availableHeight=480, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=480, performLayout=true, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=2, generationCount=8, reason=kStretch) at Yoga.cpp:3118:15
    frame #30: 0x000000010039756c RNTester-macOS`YGLayoutNodeInternal(node=0x00000001021466f0, availableWidth=640, availableHeight=480, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=480, performLayout=true, reason=kStretch, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=2, generationCount=8) at Yoga.cpp:3867:5
    frame #31: 0x0000000100399776 RNTester-macOS`YGNodelayoutImpl(node=0x000000010160d5d0, availableWidth=640, availableHeight=480, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=480, performLayout=true, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=1, generationCount=8, reason=kInitial) at Yoga.cpp:3118:15
    frame #32: 0x000000010039756c RNTester-macOS`YGLayoutNodeInternal(node=0x000000010160d5d0, availableWidth=640, availableHeight=480, ownerDirection=YGDirectionLTR, widthMeasureMode=YGMeasureModeExactly, heightMeasureMode=YGMeasureModeExactly, ownerWidth=640, ownerHeight=480, performLayout=true, reason=kInitial, config=0x0000600000c8d440, layoutMarkerData=0x0000700001215e00, layoutContext=0x0000000000000000, depth=1, generationCount=8) at Yoga.cpp:3867:5
    frame #33: 0x000000010039be82 RNTester-macOS`::YGNodeCalculateLayoutWithContext(node=0x000000010160d5d0, ownerWidth=640, ownerHeight=480, ownerDirection=YGDirectionLTR, layoutContext=0x0000000000000000) at Yoga.cpp:4123:7
    frame #34: 0x000000010039ceb3 RNTester-macOS`::YGNodeCalculateLayout(node=0x000000010160d5d0, ownerWidth=640, ownerHeight=480, ownerDirection=YGDirectionLTR) at Yoga.cpp:4225:3
    frame #35: 0x0000000100198bfd RNTester-macOS`-[RCTShadowView layoutWithMinimumSize:maximumSize:layoutDirection:layoutContext:](self=0x00000001026041c0, _cmd="layoutWithMinimumSize:maximumSize:layoutDirection:layoutContext:", minimumSize=(width = 0, height = 0), maximumSize=(width = 640, height = 480), layoutDirection=NSUserInterfaceLayoutDirectionLeftToRight, layoutContext=RCTLayoutContext @ 0x0000700001216060) at RCTShadowView.m:285:3
    frame #36: 0x00000001001884f9 RNTester-macOS`-[RCTRootShadowView layoutWithAffectedShadowViews:](self=0x00000001026041c0, _cmd="layoutWithAffectedShadowViews:", affectedShadowViews=0x0000600003307ca0) at RCTRootShadowView.m:34:3
    frame #37: 0x00000001001c299c RNTester-macOS`-[RCTUIManager uiBlockWithLayoutUpdateForRootView:](self=0x0000600002609c20, _cmd="uiBlockWithLayoutUpdateForRootView:", rootShadowView=0x00000001026041c0) at RCTUIManager.m:520:3
    frame #38: 0x00000001001cb81a RNTester-macOS`-[RCTUIManager _layoutAndMount](self=0x0000600002609c20, _cmd="_layoutAndMount") at RCTUIManager.m:1163:22
    frame #39: 0x00000001001cb634 RNTester-macOS`-[RCTUIManager batchDidComplete](self=0x0000600002609c20, _cmd="batchDidComplete") at RCTUIManager.m:1146:3
    frame #40: 0x00000001000f1ed7 RNTester-macOS`__32-[RCTCxxBridge batchDidComplete]_block_invoke(.block_descriptor=0x0000600000cf98c0) at RCTCxxBridge.mm:1380:9
    frame #41: 0x00000001014a9844 libdispatch.dylib`_dispatch_call_block_and_release + 12
    frame #42: 0x00000001014aa826 libdispatch.dylib`_dispatch_client_callout + 8
    frame #43: 0x00000001014b1dd7 libdispatch.dylib`_dispatch_lane_serial_drain + 777
    frame #44: 0x00000001014b2b90 libdispatch.dylib`_dispatch_lane_invoke + 438
    frame #45: 0x00000001014bffe0 libdispatch.dylib`_dispatch_workloop_worker_thread + 691
    frame #46: 0x0000000101538361 libsystem_pthread.dylib`_pthread_wqthread + 290
    frame #47: 0x000000010153749b libsystem_pthread.dylib`start_wqthread + 15

@ghost ghost removed the Needs: Author Feedback label Jun 7, 2020
@anandrajeswaran
Copy link

@tido64 so then the real bug would be the shadow view should not be dealing with color attributes at all since that doesn't affect sizing?

@tido64 tido64 force-pushed the tido64/fix-appearance-ui-thread branch from df1dd35 to 35d57dd Compare June 7, 2020 22:54
@tido64
Copy link
Member Author

tido64 commented Jun 7, 2020

@tido64 so then the real bug would be the shadow view should not be dealing with color attributes at all since that doesn't affect sizing?

You're right. It seems that we only need colorSpaceName here. I don't know if _aquaColor and _darkAquaColor can have different values for colorSpaceName so I kept _effectiveColor. Let me know if we should just cache colorSpaceName instead.

@tido64 tido64 changed the title Allow -[RCTDynamicColor effectiveColor] to be called off UI thread Fix -[RCTDynamicColor effectiveColor] being called off UI thread Jun 7, 2020
@anandrajeswaran
Copy link

@tido64 can we go one more level up? RCTDynamicColor should be able to be completely unaware of other threads. RCTBaseTextShadowView is the one that is asking for it on a background thread and that's what we should fix

@tido64
Copy link
Member Author

tido64 commented Jun 8, 2020

@tido64 can we go one more level up? RCTDynamicColor should be able to be completely unaware of other threads. RCTBaseTextShadowView is the one that is asking for it on a background thread and that's what we should fix

RCTBaseTextShadowView is simply creating an NSAttributedString here:

Libraries/Text/BaseText/RCTBaseTextShadowView.m:100

      if (text) {
        NSAttributedString *rawTextAttributedString =
          [[NSAttributedString alloc] initWithString:[textAttributes applyTextAttributesToText:text]
                                          attributes:textAttributes.effectiveTextAttributes];
        [attributedText appendAttributedString:rawTextAttributedString];
      }

This piece of code works fine with any NSColor value. I think the correct thing to do here is to make RCTDynamicColor behave more like NSColor. I don't know this code base very well but as far as I can tell, we would have to rewrite RCTTextShadowViewMeasure to not create NSAttributedStrings and instead use CoreText directly to measure text.

@anandrajeswaran
Copy link

@tido64 or just not include color attributes? We still should not be referring to NSColors on background threads even if that currently happens to not throw an error of some sort.

@tom-un
Copy link
Collaborator

tom-un commented Jun 8, 2020

The [RCTDynamicColor effectiveColor] method is being called because [NSColor colorSpaceName] is used in NSColor's NSObject hash function. And the hash is being called because NSAttributedString's implementation uses hashes for its attribute dictionary.

A simple fix is to override hash in RCTDynamicColor. I tried it and it works:

- (NSUInteger)hash {
  NSUInteger prime = 31, result = 1;
  result = prime * result + [_aquaColor hash];
  result = prime * result + [_darkAquaColor hash];
  return result;
}

When overriding hash we should also override isEqual:, like:

- (BOOL)isEqual:(id)other {
    if (other == self)
      return YES;
    if (!other || ![other isKindOfClass:[self class]])
      return NO;
    return [self isEqualToDynamicColor:other];
}

- (BOOL)isEqualToDynamicColor:(RCTDynamicColor *)other {
    if (self == other)
      return YES;
    if ([_aquaColor isNotEqualTo:other->_aquaColor])
      return NO;
    if ([_darkAquaColor isNotEqualTo:other->_darkAquaColor])
      return NO;
    return YES;
}

Long term react-native layout should be manipulating the NSAttributedStrings and NSColor/UIColors only on the main thread, but such a change is way out of scope of this fix. But such a think can and probably is being done as part of the Fabric rearchitecture.

@tido64 tido64 force-pushed the tido64/fix-appearance-ui-thread branch from 35d57dd to 85dca79 Compare June 8, 2020 20:33
@tido64
Copy link
Member Author

tido64 commented Jun 8, 2020

A simple fix is to override hash in RCTDynamicColor. I tried it and it works:

Thanks, @tom-un. Your solution does work.

@tom-un tom-un merged commit fdeb6d2 into microsoft:master Jun 11, 2020
@tido64 tido64 deleted the tido64/fix-appearance-ui-thread branch June 12, 2020 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dynamic colors cause a Main Thread Checker violation

4 participants