Texture
Texture copied to clipboard
ios13 -[ASTextKitContext initWithAttributedString:lineBreakMode:maximumNumberOfLines:exclusionPaths:constrainedSize:] deadlock appear in rare cases
in ios13 we found -[ASTextKitContext initWithAttributedString:lineBreakMode:maximumNumberOfLines:exclusionPaths:constrainedSize:] will case a deadlock in rare cases here is the call stacks in xcode when deadlock appears
Thread 1 Queue : com.apple.main-thread (serial)
#0 0x0000000184467cdc in __psynch_mutexwait ()
#1 0x000000018438a1a4 in _pthread_mutex_firstfit_lock_wait ()
#2 0x000000018438a114 in _pthread_mutex_firstfit_lock_slow$VARIANT$armv81 ()
#3 0x00000001844e94b0 in std::__1::mutex::lock() ()
#4 0x00000001016ffa2c in ASDN::Mutex::lock() at Pods/Texture/Source/Details/ASThread.h:166
#5 0x000000010183020c in std::__1::lock_guardASDN::Mutex::lock_guard(ASDN::Mutex&) at /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/__mutex_base:104
#6 0x000000010182fed0 in std::__1::lock_guardASDN::Mutex::lock_guard(ASDN::Mutex&) at /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/__mutex_base:104
#7 0x000000010182fb64 in ::-[ASTextKitContext initWithAttributedString:lineBreakMode:maximumNumberOfLines:exclusionPaths:constrainedSize:](NSAttributedString , NSLineBreakMode, NSUInteger, NSArray , CGSize) at Pods/Texture/Source/TextKit/ASTextKitContext.mm:44
#8 0x000000010183a164 in ::-[ASTextKitRenderer initWithTextKitAttributes:constrainedSize:](const ASTextKitAttributes &, CGSize) at Pods/Texture/Source/TextKit/ASTextKitRenderer.mm:63
#9 0x0000000101867a2c in rendererForAttributes(ASTextKitAttributes, CGSize) at Pods/Texture/Source/ASTextNode.mm:129
#10 0x00000001018692d0 in ::-ASTextNode _locked_rendererWithBounds: at Pods/Texture/Source/ASTextNode.mm:357
#11 0x00000001018698e4 in ::-ASTextNode calculateSizeThatFits: at Pods/Texture/Source/ASTextNode.mm:415
#12 0x000000010175c0c4 in ::-ASDisplayNode(ASLayoutSpec) calculateLayoutLayoutSpec: at Pods/Texture/Source/ASDisplayNode+LayoutSpec.mm:45
#13 0x0000000101770a04 in ::-ASDisplayNode calculateLayoutThatFits: at Pods/Texture/Source/ASDisplayNode.mm:1114
#14 0x000000010177083c in ::-[ASDisplayNode calculateLayoutThatFits:restrictedToSize:relativeToParentSize:](ASSizeRange, CGSize) at Pods/Texture/Source/ASDisplayNode.mm:1096
#15 0x00000001017566a4 in ::-ASDisplayNode(ASLayoutInternal) _u_measureNodeWithBoundsIfNecessary: at Pods/Texture/Source/ASDisplayNode+Layout.mm:371
#16 0x00000001017704d4 in ::-ASDisplayNode __layout at Pods/Texture/Source/ASDisplayNode.mm:1048
#17 0x00000001016d4220 in ::-_ASDisplayLayer layoutSublayers at Pods/Texture/Source/Details/_ASDisplayLayer.mm:99
#18 0x000000018afd495c in CA::Layer::layout_if_needed(CA::Transaction) ()
#19 0x000000018afcfff8 in -[CALayer layoutIfNeeded] ()
#20 0x00000001017727a4 in ::-ASDisplayNode _recursivelyTriggerDisplayAndBlock: at Pods/Texture/Source/ASDisplayNode.mm:1464
#21 0x00000001017719cc in ::__49+[ASDisplayNode scheduleNodeForRecursiveDisplay:]_block_invoke_2(ASDisplayNode , BOOL) at Pods/Texture/Source/ASDisplayNode.mm:1323
#22 0x00000001017eb73c in ::-ASRunLoopQueue processQueue at Pods/Texture/Source/ASRunLoopQueue.mm:265
#23 0x00000001017eb1e4 in ::__56-[ASRunLoopQueue initWithRunLoop:retainObjects:handler:]_block_invoke(CFRunLoopObserverRef, CFRunLoopActivity) at Pods/Texture/Source/ASRunLoopQueue.mm:153
#24 0x00000001845ec4f8 in CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION ()
#25 0x00000001845e73e4 in __CFRunLoopDoObservers ()
#26 0x00000001845e79b0 in __CFRunLoopRun ()
#27 0x00000001845e716c in CFRunLoopRunSpecific ()
#28 0x000000018e41f328 in GSEventRunModal ()
#29 0x0000000188651d0c in UIApplicationMain ()
#30 0x0000000100c7fe44 in main at QingTui_iPhone/main.m:18
#31 0x0000000184472424 in start ()
JavaScriptCore bmalloc scavenger (6)#0 0x0000000184467c8c in __psynch_cvwait ()
#1 0x00000001843894e4 in _pthread_cond_wait$VARIANT$armv81 ()
#2 0x00000001844b97a8 in std::__1::condition_variable::wait(std::__1::unique_lockstd::__1::mutex&) ()
#3 0x00000001931c669c in void std::__1::condition_variable_any::wait<std::__1::unique_lockbmalloc::Mutex >(std::__1::unique_lockbmalloc::Mutex&) ()
#4 0x00000001931ca010 in bmalloc::Scavenger::threadRunLoop() ()
#5 0x00000001931c9cf8 in bmalloc::Scavenger::threadEntryPoint(bmalloc::Scavenger) ()
#6 0x00000001931cb004 in void std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_deletestd::__1::__thread_struct >, void ()(bmalloc::Scavenger), bmalloc::Scavenger*> >(void*) ()
#7 0x000000018438c1f0 in _pthread_start ()
com.apple.uikit.eventfetch-thread (7)#0 0x00000001844465f4 in mach_msg_trap ()
#1 0x0000000184445a60 in mach_msg ()
#2 0x00000001845ec918 in __CFRunLoopServiceMachPort ()
#3 0x00000001845e7a38 in __CFRunLoopRun ()
#4 0x00000001845e716c in CFRunLoopRunSpecific ()
#5 0x0000000184927408 in -[NSRunLoop(NSRunLoop) runMode:beforeDate:] ()
#6 0x00000001849272e8 in -[NSRunLoop(NSRunLoop) runUntilDate:] ()
#7 0x00000001886e8aac in -[UIEventFetcher threadMain] ()
#8 0x0000000184a57884 in NSThread__start ()
#9 0x000000018438c1f0 in _pthread_start ()
com.apple.CFSocket.private (10)#0 0x0000000184468148 in __select ()
#1 0x00000001845fa3ec in __CFSocketManager ()
#2 0x000000018438c1f0 in _pthread_start ()
WCDB-checkpoint (11)#0 0x0000000184467c8c in __psynch_cvwait ()
#1 0x00000001843894e4 in _pthread_cond_wait$VARIANT$armv81 ()
#2 0x00000001844b97a8 in std::__1::condition_variable::wait(std::__1::unique_lockstd::__1::mutex&) ()
#3 0x00000001077ad3b0 in WCDB::TimedQueue<std::__1::basic_string<char, std::__1::char_traits
We face the same issue (we use the branch release/p.37, iOS 13.1). When we comment out [collectionNode waitUntilAllUpdatesAreProcessed];
from our code base (we call this function to wait for the result of collectionNode performBatchAnimated:
when loading data into the collection node), leading to another deadlock in ASDataController.
We used Texture heavily in our apps, with hundred of nodes on a screen. Thus, this issue makes the app crashed after just a few taps. Can anyone please help?
Is there already a solution for this issue? We experiencing some deadlocks as well since we build for IOS13
I'm getting lots of this issues as well. Is there anything I can do to help?
Try our PR to see if the problem gone https://github.com/TextureGroup/Texture/pull/1710 ;-)
Unfortunately not :-(
I get stuck overhere:
Thread 1 Queue : com.apple.main-thread (serial)
#0 0x00000001a1b94d14 in __psynch_mutexwait ()
#1 0x00000001a1ab9b70 in _pthread_mutex_firstfit_lock_wait ()
#2 0x00000001a1ab9adc in _pthread_mutex_firstfit_lock_slow ()
#3 0x00000001a1c1c030 in std::__1::mutex::lock() ()
#4 0x0000000102243fa8 in ASDN::Mutex::lock() at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/Details/ASThread.h:166
#5 0x00000001023741d8 in std::__1::lock_guard<ASDN::Mutex>::lock_guard(ASDN::Mutex&) at /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/__mutex_base:104
#6 0x0000000102373e9c in std::__1::lock_guard<ASDN::Mutex>::lock_guard(ASDN::Mutex&) at /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/__mutex_base:104
#7 0x0000000102373b30 in ::-[ASTextKitContext initWithAttributedString:lineBreakMode:maximumNumberOfLines:exclusionPaths:constrainedSize:](NSAttributedString *, NSLineBreakMode, NSUInteger, NSArray *, CGSize) at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/TextKit/ASTextKitContext.mm:44
#8 0x000000010237e130 in ::-[ASTextKitRenderer initWithTextKitAttributes:constrainedSize:](const ASTextKitAttributes &, CGSize) at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/TextKit/ASTextKitRenderer.mm:63
#9 0x00000001023abc18 in rendererForAttributes(ASTextKitAttributes, CGSize) at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/ASTextNode.mm:129
#10 0x00000001023ad4bc in ::-[ASTextNode _locked_rendererWithBounds:](CGRect) at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/ASTextNode.mm:357
#11 0x00000001023adad0 in ::-[ASTextNode calculateSizeThatFits:](CGSize) at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/ASTextNode.mm:415
#12 0x00000001022a072c in ::-[ASDisplayNode(ASLayoutSpec) calculateLayoutLayoutSpec:](ASSizeRange) at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/ASDisplayNode+LayoutSpec.mm:45
#13 0x00000001022b4f94 in ::-[ASDisplayNode calculateLayoutThatFits:](ASSizeRange) at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/ASDisplayNode.mm:1114
#14 0x00000001022b4dcc in ::-[ASDisplayNode calculateLayoutThatFits:restrictedToSize:relativeToParentSize:](ASSizeRange, CGSize) at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/ASDisplayNode.mm:1096
#15 0x000000010229ad0c in ::-[ASDisplayNode(ASLayoutInternal) _u_measureNodeWithBoundsIfNecessary:](CGRect) at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/ASDisplayNode+Layout.mm:371
#16 0x00000001022b4a64 in ::-[ASDisplayNode __layout]() at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/ASDisplayNode.mm:1048
#17 0x000000010221880c in ::-[_ASDisplayLayer layoutSublayers]() at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/Details/_ASDisplayLayer.mm:99
#18 0x00000001a88863fc in CA::Layer::layout_if_needed(CA::Transaction*) ()
#19 0x00000001a62d341c in -[UIView(Hierarchy) layoutBelowIfNeeded] ()
#20 0x00000001a62d9f60 in +[UIView(Animation) performWithoutAnimation:] ()
#21 0x00000001a602ba84 in -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] ()
#22 0x00000001a5ff8704 in -[UITableView _updateVisibleCellsNow:] ()
#23 0x00000001a6015e10 in -[UITableView layoutSubviews] ()
#24 0x000000010235cd14 in ::-[ASTableView layoutSubviews]() at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/ASTableView.mm:777
#25 0x00000001a62e7abc in -[UIView(CALayerDelegate) layoutSublayersOfLayer:] ()
#26 0x00000001a1abfaf0 in -[NSObject performSelector:withObject:] ()
#27 0x00000001a88860f4 in -[CALayer layoutSublayers] ()
#28 0x00000001022187dc in ::-[_ASDisplayLayer layoutSublayers]() at /Users/baskrijgsman/appname-ios/Pods/Texture/Source/Details/_ASDisplayLayer.mm:97
#29 0x00000001a88863fc in CA::Layer::layout_if_needed(CA::Transaction*) ()
#30 0x00000001a8899964 in CA::Layer::layout_and_display_if_needed(CA::Transaction*) ()
#31 0x00000001a87dec1c in CA::Context::commit_transaction(CA::Transaction*, double) ()
#32 0x00000001a8809bd8 in CA::Transaction::commit() ()
#33 0x00000001a874333c in CA::Display::DisplayLink::dispatch_items(unsigned long long, unsigned long long, unsigned long long) ()
#34 0x00000001a8811d30 in display_timer_callback(__CFMachPort*, void*, long, void*) ()
#35 0x00000001a1cfaf34 in __CFMachPortPerform ()
#36 0x00000001a1d2591c in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ ()
#37 0x00000001a1d24fe8 in __CFRunLoopDoSource1 ()
#38 0x00000001a1d1fc20 in __CFRunLoopRun ()
#39 0x00000001a1d1f098 in CFRunLoopRunSpecific ()
#40 0x00000001abe89534 in GSEventRunModal ()
#41 0x00000001a5e3f7ac in UIApplicationMain ()
#42 0x000000010099c970 in main at /Users/baskrijgsman/appname-ios/AppName/AppNameDelegate.swift:27
#43 0x00000001a1b9ef30 in start ()
Try our PR to see if the problem gone #1710 ;-)
Also not working for me either :(
Investigation into deadlocks with iOS13 text rendering
Attn: @businessengine @esam091 @bkrijgsman @CoderXL
We're looking into this at Pinterest since we're also hitting the issue. A couple things have popped out in our investigation:
Possibly unnecessary global mutex
- The usage of a global mutex for all text kit renderering seems to be unnecessary at this time and quite possibly has been for a while.
Have you tried enabling the
ASExperimentalRemoveTextKitInitialisingLock
experiment? ASTextKitContext shares a single global mutex which we believe is contributing to deadlocks.
I wrote a simple unit test to make sure that allocating thousands of ASTextKitContent objects did not deadlock when this mutex was disabled. This test gives us decent confidence that the root cause of the global mutex (written in 2015) might have been resolved a while ago.
- (void)testTextKitComponentsCanBeAllocatedConcurrently
{
ASConfiguration *config = [ASConfiguration new];
config.experimentalFeatures = ASExperimentalRemoveTextKitInitialisingLock;
[ASConfigurationManager test_resetWithConfiguration:config];
NSAttributedString *attributedString =
[[NSAttributedString alloc]
initWithString:@"90's cray photo booth tote bag bespoke Carles. Plaid wayfarers Odd Future master cleanse tattooed four dollar toast small batch kale chips leggings meh photo booth occupy irony. " attributes:@{}];
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
for (NSUInteger i = 0; i < 5000; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:@"Allocate textkit content"];
dispatch_async(queue, ^{
ASTextKitContext *context = [[ASTextKitContext alloc]
initWithAttributedString:attributedString
tintColor:nil
lineBreakMode:NSLineBreakByWordWrapping
maximumNumberOfLines:3
exclusionPaths:nil
constrainedSize:CGSizeMake(100, 100)];
XCTAssert(context != nil);
[expectation fulfill];
});
}
[self waitForExpectationsWithTimeout:60 handler:nil];
}
Please test out that experiment and let us know if you're still able to reproduce the issue
Deadlock in background layout calculation
-
ASDataController
seems to be calculating layouts in the background. For layout calculation of text nodes,NSTextStorage
appears to be blocked waiting for the main thread when it's attributed string is set. The Data Controller is waiting for the nodes to concurrently calculate layout and blocks until finished. If those layout calculations require work to be performed on main then we have a deadlock.
For ASTextNode
, the text kit renderer abstraction has a class called ASTextKitContext
which creates and owns the underlying NSTextStorage
/ NSLayoutManager
instances for a given text node. When the attributed string is set on NSTextStorage
it appears to post notifications for NSTextStorageWillProcessEditingNotification
and NSTextStorageDidProcessEditingNotification
on the main thread and blocks the current background thread until finished.
This leads to a few problems
- Deadlock since the data controller is waiting for cell layout calculations to finish (
-[ASDataController updateWithChangeSet:]
) - Text nodes are waiting for
+[NSOperationQueue mainQueue]
to process a notification from the background layout (see-[NSOperation waitUntilFinished]
)
Thread 135 Queue : com.apple.root.user-initiated-qos (concurrent)
#0 0x0000000184467c8c in __psynch_cvwait ()
#1 0x00000001843894e4 in _pthread_cond_wait$VARIANT$armv81 ()
#2 0x00000001849b22e4 in -[NSOperation waitUntilFinished] ()
#3 0x000000018494e8c8 in -[__NSObserver _doit:] ()
#4 0x00000001845ca9ac in CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER ()
#5 0x00000001845ca9f4 in ___CFXRegistrationPost1_block_invoke ()
#6 0x00000001845c9d54 in _CFXRegistrationPost1 ()
#7 0x00000001845c9a20 in ___CFXNotificationPost_block_invoke ()
#8 0x0000000184545d38 in -[_CFXNotificationRegistrar find:object:observer:enumerator:] ()
#9 0x00000001845c9370 in _CFXNotificationPost ()
#10 0x0000000184925cf8 in -[NSNotificationCenter postNotificationName:object:userInfo:] ()
#11 0x0000000187ba2b44 in -[NSTextStorage processEditing] ()
#12 0x0000000187ba337c in -[NSTextStorage edited:range:changeInLength:] ()
#13 0x0000000184979d60 in -[NSConcreteMutableAttributedString replaceCharactersInRange:withAttributedString:] ()
#14 0x0000000187c241ac in __71-[NSConcreteTextStorage replaceCharactersInRange:withAttributedString:]_block_invoke ()
#15 0x0000000187c23ddc in __NSConcreteTextStorageLockedForwarding ()
#16 0x0000000187ba90e4 in -[NSConcreteTextStorage replaceCharactersInRange:withAttributedString:] ()
#17 0x000000010182fc74 in ::-[ASTextKitContext initWithAttributedString:lineBreakMode:maximumNumberOfLines:exclusionPaths:constrainedSize:]
- Unbounded growth in threads since every text rendering action is blocked either from the deadlock or mutex which results in new threads created to continue text rendering calculations.
We're currently discussing the right fix but I believe this is what we're seeing with the adoption of iOS 13.
Hope this helps!
As @rahul-malik mentioned, when we looked into this it appeared that NSTextStorage
was calling out to another thread to run an operation and then sitting there waiting for it:
#2 0x00000001849b22e4 in -[NSOperation waitUntilFinished] ()
#3 0x000000018494e8c8 in -[__NSObserver _doit:] ()
#4 0x00000001845ca9ac in CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER ()
#5 0x00000001845ca9f4 in ___CFXRegistrationPost1_block_invoke ()
#6 0x00000001845c9d54 in _CFXRegistrationPost1 ()
#7 0x00000001845c9a20 in ___CFXNotificationPost_block_invoke ()
#8 0x0000000184545d38 in -[_CFXNotificationRegistrar find:object:observer:enumerator:] ()
#9 0x00000001845c9370 in _CFXNotificationPost ()
#10 0x0000000184925cf8 in -[NSNotificationCenter postNotificationName:object:userInfo:] ()
From the above call stack, it looks like _doit:
is the one that actually kicks off the operation. The next method, waitUntilFinished
, is where we start synchronously waiting for it to finish. I swizzled _doit:
to try to get a better look at __NSObserver
and see what it is doing. From a header dump, I could see that __NSObserver
has a queue
member. Inspecting that queue
prints outs:
Printing description of _self->queue:
<NSOperationQueue: 0x109b497e0>{name = 'NSOperationQueue Main Queue'}
This seemed to back up our theory that this operation was being queued on the main thread while the main thread was synchronously waiting for the posting thread to finish layout. We were also able to verify that the notification that is being handled is either NSTextStorageWillProcessEditingNotification
or NSTextStorageDidProcessEditingNotification
. I don't know if this notification has started jumping to main in iOS13, or if the likelihood of it jumping to main has increased, but we are seeing deadlocks more in iOS13 than other versions.
The long term fix for this type of deadlock is probably to stop relying on ASMainSerialQueue
-- that is stop synchronously waiting forever on main for anything. You can never know when the things you are waiting for will need to synchronously wait for something to run on main!
Our proposed quick fix for this is kind of gross, but so far seems to be working. Basically we are swizzling __NSObserver
's _doit:
method and dispatching asynchronously to main any time the notification that we are "doing" is either NSTextStorageWillProcessEditingNotification
or NSTextStorageDidProcessEditingNotification
. It looks something like this:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// we are swizzling the __NSObserver class (you may want to obfuscate this string)
Class class = [NSClassFromString(@"__NSObserver") class];
// we are swizzling the doit: method (you may want to obfuscate this string)
SEL originalSelector = NSSelectorFromString(@"_doit:");
SEL swizzledSelector = NSSelectorFromString(@"swizzled_doit:");
Method origMethod = class_getInstanceMethod(class, originalSelector);
// Create a new implementation via a block
IMP impl = imp_implementationWithBlock(^(id _self, id arg) {
NSNotification *notification = (NSNotification *)arg;
// make sure arg is a notification, then check to see if it is NSTextStorageDidProcessEditingNotification or NSTextStorageDidProcessEditingNotification
if ([notification isKindOfClass:[NSNotification class]] && notification &&
([[notification name] isEqualToString:NSTextStorageDidProcessEditingNotification] || [[notification name] isEqualToString:NSTextStorageWillProcessEditingNotification])) {
// This is the notification we are looking for. _doit: will jump to the main thread and wait until the operation is attended to.
// Instead, let's dispatch doit: asynchrously to the main thread. that will allow layout to continue so main will stop blocking
// and then _doit: will eventually run on main.
dispatch_async(dispatch_get_main_queue(),^{
((void (*)(id, SEL, NSNotification *))objc_msgSend)(_self, swizzledSelector, arg);
});
} else {
// This isn't the notification we are looking for. Proceed without any dispatching to main
((void (*)(id, SEL, NSNotification *))objc_msgSend)(_self, swizzledSelector, arg);
}
});
// add the new method to the class
if (class_addMethod(class, swizzledSelector, impl, method_getTypeEncoding(origMethod))) {
Method newMethod = class_getInstanceMethod(class, swizzledSelector);
// If original doesn't implement the method we want to swizzle, create it.
if (class_addMethod(class, originalSelector, method_getImplementation(newMethod), method_getTypeEncoding(origMethod))) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(origMethod), method_getTypeEncoding(newMethod));
} else {
method_exchangeImplementations(origMethod, newMethod);
}
}
});
My biggest concern about this is that UIKit must be dispatching to main and waiting for some reason. Whatever that reason may be, we are basically ignoring it with this change. But UIKit probably doesn't expect main to be synchronously waiting for anything, and layout on a background thread is so non-UIKit that perhaps this change will end up being innocuous.
Great findings on the (new?) behavior of NSTextStorage you all.
I would like to chime in a bit to provide an insight on the default behavior of ASDataController, hopefully without distracting you from the discussion for the right fix. By default ASDataController does not block main thread while processing a batch update on background thread(s), unless the number of new cell nodes is 0 or 1 (although I think we can get rid of this caveat).
Clients can change the default behavior by either setting cell layout mode to ASCellLayoutModeAlwaysSync
, or calling -waitUntilAllUpdatesAreProcessed
on the collection/table node.
It's true that if ASDataController blocks main and one of the background threads (that is responsible for processing the batch update) tries to dispatch sync back to main, we’ll hit a deadlock. This means -waitUntilAllUpdatesAreProcessed is no longer a safe API. So I agree with @rcancro that the long term fix is probably to stop synchronously waiting forever on main, essentially getting rid of -waitUntilAllUpdatesAreProcessed
and perhaps ASMainSerialQueue
.
Edit: clarify the default behavior of ASDataController and my thought on the long term fix.
Thanks for the clarification @rahul-malik, I'm gonna try some solutions based on what you explained.
@esam091 Have you find any solution yet?
We ended up replacing ASTextNode with ASTextNode2 on screens that have the problem. The problem isn't totally gone but significantly reduced for now.
Amazing that worked really well. We couldn't trigger a freeze ourselves anymore.
@esam091 Could you please verify if ASTextNode2 doesn't have this issue and the issue isn't totally gone because you still have ASTextNode somewhere else in your app? Or that ASTextNode2 still hits this deadlock occasionally?
I believe the former is the case since ASTextNode uses TextKit which has a new behavior on iOS 13 that causes NSTextStorage to deadlock, and ASTextNode2 doesn't use TextKit but CoreText directly.
We do still have a lot of ASTextNodes, and only change them to ASTextNode2 on view controllers with lots of freezes. After talking to my coworkers, the freezes don't happen on those screens anymore, it was somewhere else. Maybe it is caused by something unrelated, we're not so sure because it happens so infrequently.
Thanks for all the comments, folks. We have tried to replace ASTextNode by ASTextNode2 however the ASTextNode2 seems having performance issues, the render is noticeable slow comparing to ASTextNode. Is that expected @nguyenhuy?
Hi, I think(hope :) ) I'm facing the same issue, just wanted confirm. Crash happened a few times to our QA team, randomly, (I can not reproduce it) but always in Messaging part of our app, either in InboxTVC or ThreadTVC (90% ASTextnodes) In all cases resulting the app to freeze before being terminated:
OS Version: iPhone OS 13.3.1 (17D50)
Exception Type: EXC_CRASH (SIGKILL) Exception Codes: 0x0000000000000000, 0x0000000000000000 Exception Note: EXC_CORPSE_NOTIFY Termination Reason: Namespace SPRINGBOARD, Code 0x8badf00d Termination Description: SPRINGBOARD, scene-update watchdog transgression: application<com.cmp.MyApp>:14108 exhausted real (wall clock) time allowance of 10.00 seconds | ProcessVisibility: Foreground | ProcessState: Running | WatchdogEvent: scene-update | WatchdogVisibility: Background | WatchdogCPUStatistics: ( | "Elapsed total CPU time (seconds): 17.960 (user 17.960, system 0.000), 10% CPU", | "Elapsed application CPU time (seconds): 0.448, 0% CPU" | ) Triggered by Thread: 0
With stacktrace:
Dispatch queue: com.apple.main-thread (0) #0 (null) in __psynch_mutexwait () #1 (null) in _pthread_mutex_firstfit_lock_wait () #2 (null) in _pthread_mutex_firstfit_lock_slow$VARIANT$armv81 () #3 (null) in std::__1::mutex::lock+ 255152 () () #4 0x0000000103841c3c in -[ASTextKitContext initWithAttributedString:lineBreakMode:maximumNumberOfLines:exclusionPaths:constrainedSize:] at /Pods/Texture/Source/TextKit/ASTextKitContext.mm:46 #5 0x0000000103846a70 in -[ASTextKitRenderer initWithTextKitAttributes:constrainedSize:] at /Pods/Texture/Source/TextKit/ASTextKitRenderer.mm:63 #6 (null) in rendererForAttributes(ASTextKitAttributes, CGSize) () #7 0x000000010385e640 in -[ASTextNode _locked_rendererWithBounds:] at /Pods/Texture/Source/ASTextNode.mm:357 #8 (null) in -[ASTextNode calculateSizeThatFits:] () #9 0x00000001037e479c in -[ASDisplayNode(ASLayoutSpec) calculateLayoutLayoutSpec:] at /Pods/Texture/Source/ASDisplayNode+LayoutSpec.mm:47 #10 (null) in -[ASDisplayNode calculateLayoutThatFits:] () #11 0x00000001037efb48 in -[ASDisplayNode calculateLayoutThatFits:restrictedToSize:relativeToParentSize:] at /Pods/Texture/Source/ASDisplayNode.mm:1106 #12 (null) in -[ASDisplayNode(ASLayoutElement) layoutThatFits:parentSize:] () #13 (null) in -[ASRelativeLayoutSpec calculateLayoutThatFits:] () #14 0x000000010380dbb4 in -[ASLayoutSpec calculateLayoutThatFits:restrictedToSize:relativeToParentSize:] at /Pods/Texture/Source/Layout/ASLayoutSpec.mm:80 #15 (null) in -[ASLayoutSpec layoutThatFits:parentSize:] () #16 (null) in crossChildLayout(ASStackLayoutSpecChild const&, ASStackLayoutSpecStyle const&, double, double, double, double, CGSize) () #17 (null) in invocation function for block in layoutItemsAlongUnconstrainedStackDimension(std::__1::vector<ASStackLayoutSpecItem, std::__1::allocator<ASStackLayoutSpecItem> >&, ASStackLayoutSpecStyle const&, bool, ASSizeRange const&, CGSize, bool) () #18 0x000000010382d0b4 in dispatchApplyIfNeeded(unsigned long, bool, void (unsigned long) block_pointer) at /Pods/Texture/Source/Private/Layout/ASStackUnpositionedLayout.mm:83 #19 0x000000010382c304 in ASStackUnpositionedLayout::compute(std::__1::vector<ASStackLayoutSpecChild, std::__1::allocator<ASStackLayoutSpecChild> > const&, ASStackLayoutSpecStyle const&, ASSizeRange const&, bool) at /Pods/Texture/Source/Private/Layout/ASStackUnpositionedLayout.mm:734 #20 0x000000010382ac64 in -[ASStackLayoutSpec calculateLayoutThatFits:] at /Pods/Texture/Source/Layout/ASStackLayoutSpec.mm:149 #21 0x000000010380dbb4 in -[ASLayoutSpec calculateLayoutThatFits:restrictedToSize:relativeToParentSize:] at /Pods/Texture/Source/Layout/ASLayoutSpec.mm:80 #22 (null) in -[ASLayoutSpec layoutThatFits:parentSize:] () #23 (null) in -[ASInsetLayoutSpec calculateLayoutThatFits:restrictedToSize:relativeToParentSize:] () #24 (null) in -[ASLayoutSpec layoutThatFits:parentSize:] () #25 (null) in -[ASDisplayNode(ASLayoutSpec) calculateLayoutLayoutSpec:] () #26 (null) in -[ASDisplayNode calculateLayoutThatFits:] () #27 0x00000001037efb48 in -[ASDisplayNode calculateLayoutThatFits:restrictedToSize:relativeToParentSize:] at /Pods/Texture/Source/ASDisplayNode.mm:1106 #28 (null) in -[ASDisplayNode(ASLayoutElement) layoutThatFits:parentSize:] () #29 (null) in -[ASTableView didLayoutSubviewsOfTableViewCell:] () #30 0x0000000103832e30 in -[_ASTableViewCell layoutSubviews] at /Pods/Texture/Source/ASTableView.mm:89 #31 (null) in -[UIView+ 15307132 (CALayerDelegate) layoutSublayersOfLayer:] () #32 (null) in -[CALayer layoutSublayers] () #33 (null) in CA::Layer::layout_if_needed+ 1406012 (CA::Transaction*) () #34 (null) in -[UIView+ 15225904 (Hierarchy) layoutBelowIfNeeded] () #35 (null) in +[UIView+ 15252500 (Animation) performWithoutAnimation:] () #36 (null) in -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] () #37 (null) in -[UITableView _createPreparedCellForGlobalRow:willDisplay:] () #38 (null) in -[_UITableViewUpdateSupport+ 12651728 (Private) _setupAnimationsForNewlyInsertedCells] () #39 (null) in -[_UITableViewUpdateSupport _setupAnimations] () #40 (null) in -[UITableView _updateWithItems:updateSupport:] () #41 (null) in -[UITableView _endCellAnimationsWithContext:] () #42 (null) in -[UITableView endUpdatesWithContext:] () #43 0x000000010383a378 in -[ASTableView rangeController:updateWithChangeSet:updates:] at /Pods/Texture/Source/ASTableView.mm:1655 #44 0x0000000103826700 in -[ASRangeController dataController:updateWithChangeSet:updates:] at /Pods/Texture/Source/Details/ASRangeController.mm:517 #45 0x00000001037da818 in __40-[ASDataController updateWithChangeSet:]_block_invoke_2.193 at /Pods/Texture/Source/Details/ASDataController.mm:665 #46 0x0000000103811558 in __30-[ASMainSerialQueue runBlocks]_block_invoke at /Pods/Texture/Source/Details/ASMainSerialQueue.mm:68 #47 (null) in _dispatch_call_block_and_release () #48 (null) in _dispatch_client_callout () #49 (null) in _dispatch_main_queue_callback_4CF$VARIANT$armv81 () #50 (null) in CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE () #51 (null) in __CFRunLoopRun () #52 (null) in CFRunLoopRunSpecific () #53 (null) in GSEventRunModal () #54 (null) in UIApplicationMain () #55 0x000000010297b018 in main at /MyApp/AppDelegate.swift:23 #56 (null) in start ()
I've tried removing locks here and here, and the deadlocks seem to be gone just as @rahul-malik said https://github.com/TextureGroup/Texture/issues/1690#issuecomment-558481202. I'm not sure whether this is a safe solution though 😅.
Maybe @steewsc and @bkrijgsman want to give it a try?
@esam091 thank you, but I've already replaced all ASTextNodes and ASButtonNodes with ASTextNode2 and there are no crashes so far.
Hi @esam091 @businessengine
Any luck with the fix? I am still getting this in iOS 14 and above.
I have tried removing the locks suggested in this comment https://github.com/TextureGroup/Texture/issues/1690#issuecomment-610843489
Above removal of lock removed the issue in iOS 13. But it started occurring again in iOS 14 and above.
Please help this if you have any solutions.
We also saw this come back in iOS14. We were able to get around it by swizzling farther up the stack in iOS13. Instead of swizzling _doit:
we swizzled postNotificationName:object:userInfo:
. Looking at the code above, I think you'll want to make these changes (I didn't test this code 🤞 ):
// we are swizzling the NSNotificationCenter class
Class class = [NSClassFromString(@"NSNotificationCenter") class];
// we are swizzling the postNotificationName:object:userInfo: method
SEL postSelector = NSSelectorFromString(@"postNotificationName:object:userInfo:");
SEL piPostSelector = NSSelectorFromString(@"swizzled_postNotificationName:object:userInfo:");
Once this is in you can remove the _doit:
swizzle.
iOS 15 now but this issue is still happening with ASTextNode
. Did anyone resolve it with a less-intrusive approach than swizzling -[__NSObserver _doit:]
? We've considered using ASTextNode2
instead, but we find its rendering and layout is quite unstable.
Just a word of caution to anyone using the swizzling solutions above:
I've found that it messes up the delegate events in some UIKit controls. For example in UISearchBar
, the UISearchToken
s disappear because internally the UISearchTextField.tokens
do not get retained in time. The result is that by the time UISearchBarDelegate
's didChangeText
is called UISearchTextField.tokens
will always be empty, preventing any reliable state management.
FYI, I think I know why UIKit runs NSTextStorage
processing synchronously. It turns out NSTextStorage
is a class-cluster, and our swizzling workarounds in this thread should only be used on "NSConcreteTextStorage"
. Other types such as _UICascadingTextStorage
(used by UISearchBar
tokens for example) seem to do recursive calculations that would cause range issues (including out-of-range exceptions) if ran asynchronously.
Hello, Have we reached a consensus on how to fix this issue? I am currently struggling with these crashes. :/
Applying the swizzling suggestions in this thread AND filtering the swizzled method only for NSConcreteTextStorage
senders should help you. Below is what I use and it's pretty stable so far. If you find exceptions that should be added aside from NSConcreteTextStorage
, please do share.
@implementation ASTextNode (App)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [NSNotificationCenter class];
SEL originalSelector = @selector(postNotificationName:object:userInfo:);
SEL swizzledSelector = NSSelectorFromString(@"swizzled_postNotificationName:object:userInfo:");
Method origMethod = class_getInstanceMethod(class, originalSelector);
IMP impl = imp_implementationWithBlock(^(id _self, id name, id object, id userInfo) {
if ([name isKindOfClass:[NSString class]]
&& ([name isEqualToString:NSTextStorageDidProcessEditingNotification]
|| [name isEqualToString:NSTextStorageWillProcessEditingNotification])
&& [NSStringFromClass([object class]) isEqualToString:@"NSConcreteTextStorage"]) {
dispatch_async(dispatch_get_main_queue(), ^{
((void (*)(id, SEL, NSString *, id, NSDictionary *))objc_msgSend)(_self, swizzledSelector, name, object, userInfo);
});
}
else {
((void (*)(id, SEL,NSString *, id, NSDictionary *))objc_msgSend)(_self, swizzledSelector, name, object, userInfo);
}
});
if (class_addMethod(class, swizzledSelector, impl, method_getTypeEncoding(origMethod))) {
Method newMethod = class_getInstanceMethod(class, swizzledSelector);
if (class_addMethod(class, originalSelector, method_getImplementation(newMethod), method_getTypeEncoding(origMethod))) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(origMethod), method_getTypeEncoding(newMethod));
}
else {
method_exchangeImplementations(origMethod, newMethod);
}
}
});
}
@end