How to use UIViewLayoutFeedbackLoopDebuggingThreshold today
A while ago, Tyler Fox mentioned Auto Layout’s feedback loop debugger on Twitter, which was a feature announced back in WWDC 2016 but largely seems to have gone unnoticed since. Apple certainly weren’t trying to hide it – Marian Goldeen spent almost 15 minutes or so talking about it in this video – but with all the other big announcements coming at WWDC it’s easy for things to get lost.
Layout feedback loops don’t happen often, but you’ve almost certainly faced them at some point. They occur when your views are running their layout code, but part way through something triggers them to start their layout pass again. That might be the result of one view changing the size of one of its superviews, or perhaps because you have an ambiguous layout.
Either way, this problem manifests itself as your CPU maxing out and RAM usage steadily marching upwards, all because your views are running their layout code again and again without ever returning. Worse, if you try to debug the problem you’ll just see lots of layout code running – it’s not obvious where the loop is, or what might resolve it.
The Layout Feedback Loop Debugger is designed to solve this problem: you just set one of two launch arguments, depending on whether you want to target iOS/tvOS or macOS, then run your app as normal. Here’s how to try it out:
-UIViewLayoutFeedbackLoopDebuggingThreshold 100
-NSViewLayoutFeedbackLoopDebuggingThreshold 100
Although we’re using 100 above, you can use any number between 50 and 1000 – it represents how many times a view must layout its subviews in a single run loop before it is considered to be a feedback loop. So, by setting 100 we’re saying “allow one view to lay out itself 100 times in a loop before we acknowledge there’s a problem.”
When that limit is reached your app won’t stop immediately. Instead, the layout feedback loop debugger will let layout continue a few more times so it can take a log of exactly what’s happening, before finally raising an exception and printing a gigantic log of its findings. Even the most trivial of feedback loops generates over 500 lines of log data, so brace yourself for some detailed reading!
If you want to try it immediately and see what you get back, just create a simple layout feedback loop in your app. For example, you might add a method like this to a view controller:
override func viewDidLayoutSubviews() {
view.setNeedsLayout()
}
That will tell its view to redo its layout every time layout continues, so layout will never finish. When you run that code, your app will raise an exception after a split second, but you’ll see about 500 lines of log data in your Xcode console. The feedback loop debugger is extremely verbose, but it does tell you exactly what’s happening – in theory you should be able to leave that launch argument in place, because most of the time it won’t do anything.
Using the code above, part of your log will say this:
2018-03-11 16:11:59.148277+0000 DebugTest[9005:547143] [LayoutLoop] >>>UPSTREAM LAYOUT DIRTYING<<< About to send -setNeedsLayout to layer for <UIView: 0x7fdfb3704040; f={{0, 0}, {375, 812}} > under -viewDidLayoutSubviews for <UIView: 0x7fdfb3704040; f={{0, 0}, {375, 812}} >
(
0 UIKit 0x0000000104a168e8 -[_UIViewLayoutFeedbackLoopDebugger _recordSetNeedsLayoutToLayerOfView:] + 382
1 UIKit 0x00000001041e8407 -[UIView(Hierarchy) setNeedsLayout] + 164
2 DebugTest 0x0000000102f7bb5d _T09DebugTest14ViewControllerC21viewDidLayoutSubviewsyyF + 173
3 DebugTest 0x0000000102f7bba4 _T09DebugTest14ViewControllerC21viewDidLayoutSubviewsyyFTo + 36
4 UIKit 0x00000001041febe9 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1819
5 QuartzCore 0x000000010aaed61c -[CALayer layoutSublayers] + 159
6 QuartzCore 0x000000010aaf17ad _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 401
7 QuartzCore 0x000000010aa7886c _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 364
8 QuartzCore 0x000000010aaa5946 _ZN2CA11Transaction6commitEv + 500
9 UIKit 0x000000010414826a __34-[UIApplication _firstCommitBlock]_block_invoke_2 + 141
10 CoreFoundation 0x00000001074ac05c __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
11 CoreFoundation 0x000000010749083b __CFRunLoopDoBlocks + 203
12 CoreFoundation 0x0000000107490014 __CFRunLoopRun + 1300
13 CoreFoundation 0x000000010748f889 CFRunLoopRunSpecific + 409
14 GraphicsServices 0x0000000109c959c6 GSEventRunModal + 62
15 UIKit 0x000000010412d5d6 UIApplicationMain + 159
16 DebugTest 0x0000000102f7cda7 main + 55
17 libdyld.dylib 0x00000001086a7d81 start + 1
It’s telling you that setNeedsLayout()
is about to be called on a view, but it’s also said “>>>UPSTREAM LAYOUT DIRTYING<<<“ because that’s the root of the problem: this isn’t an ambiguous layout problem, but is instead a result of one view forcing part of its view hierarchy to redo layout.
You also get a complete back trace, which contains this call at frame 2: _T09DebugTest14ViewControllerC21viewDidLayoutSubviewsyyF
– that’s the viewDidLayoutSubviews()
method of our ViewController
class, so you can see what’s causing the loop.
If you scroll far enough down the log, you’ll see this output:
Views receiving layout in order: (
<UIView: 0x7fdfb3704040; frame = (0 0; 375 812); >
)
This section is designed to help you visualize the order in which layout was calculated. We have a trivial layout here, but for more complex layouts – when you have to scroll a long way through the log! – you’ll see a complete ordered list of how Auto Layout was working.
If you want to try the layout feedback loop debugger in your own apps, here are two last tips:
Thanks to Marian for talking about it at WWDC, and to Tyler for nudging our memory!
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.