2020 • 11:22
Discover how to render smoother animations in your app by troubleshooting the commit phase of your render loop. Dive into the mechanics of this phase, and learn how to use Instruments to uncover the source of hitches in your app, eliminate them, and avoid them outright.
Speaker: Charles Circlaeys
Downloads from Apple
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Hi, my name is Charles Circlaeys, and in this talk, we will dive into scrolling in animation hitches in the commit phase of the render loop. iOS uses the render loop to display your views. Touch events are sent to your app, it responds by changing its views, and those views are rendered onto the display by iOS. We will focus on finding and fixing animation hitches in the commit phase of the render loop. To learn about the entire render loop and what is a hitch, please watch the video "Explore UI Animation Hitches and the Render Loop".
We will first look into what defines a commit transaction. Use Instruments to find hitches. and we will share our recommendations to avoid commit hitches. Let's start by defining a commit transaction. Here, we have an example of an app's view hierarchy that is currently waiting for events. After it receives a touch event, a view responds to it and processes the event by changing the background color or the frame for some of its subviews. The system records that these subviews will require a layout or display during the next commit transaction. During the commit transaction, the views that needed display or layout will be updated accordingly by calling drawRect or layoutSubviews.
Let's take a look at the different phases involved during a commit transaction. There are four steps: the layout phase, the display phase, the prepare phase, and finally, the commit phase. During the layout phase, LayoutSubViews will be called for every view that requires a layout. You can mark a layout needed by: changing a view's position, adding or removing views, or explicitly call SetNeedsLayout on the view.
During the display phase, drawRect will be called for every view that requires display. You can indicate that display is needed by adding views to the view hierarchy that override drawRect, or by calling setNeedsDisplay explicitly. During the prepare phase, images that haven't been decoded yet will be decoded during this step. This kind of operation can take considerable time for large images.
Also, if an image is in a color format that the GPU cannot directly work with, it will be converted during this step. This will require the image to be copied instead of sending a pointer to the original, which will cost additional time and memory. To learn more about optimizing the images in your app, watch the "Image and Graphics Best Practices" video.
Finally, during the commit phase, the view hierarchy will be packaged up recursively and sent to the render server. Note that deep view hierarchies will take longer to be packaged up. Now that we have described the details of a commit transaction, let's move to our second topic: find hitches with Instruments. In Xcode 12, we released a new Instrument template to profile hitches in your apps.
This will help you to visualize and investigate the render loop for the hitching frames detected. Let's look at some hitches in our example app. We will record a trace in Instruments as we scroll in our application. So here we have a recorded trace of our scrolling performance, and we can see all the hitches detected.
Let's take a closer look at the hitch 16. We can see on the left all the tracks corresponding to the render loop phases required to compose the frames. The hitches track shows the hitches and their durations. The User Events track shows the user events associated with the hitching frame.
The commits track shows the commit phases and the processes that committed during this phase. Patrick will talk more about the renders and GPU tracks in "Demystify and Eliminate Hitches in the Render Phase". The Frame Lifetimes track shows the entire duration to compose the hitching frame. The built-in display track shows all the frames that appeared on display along with the VSync events.
You can compare the frame lifetime with the beginning of the hitch duration to visualize the expected interval that the frame should have been ready for display. This interval is called the acceptable latency. All time after that is the hitch duration. Below the tracks, we can see detailed metrics for the hitch when the hitches track is selected. There are many hitches in our demo app, but we have been focusing on the hitch 16 here.
We can see the hitch duration, the acceptable latency, and the hitch type. Hitch type is useful to get a hint during which phase the frame was delayed, and where to start investigating. For this example, we can see that the selected hitching frame was caused by the commit and the GPU phase.
I'd like to find what code is taking too long in the commit phase, and thankfully, the animation hitches template includes Time Profiler, so we can see what code is running when this hitch is occurring. From here, I can select the interval I want to investigate, and search for the process that was committing. I can select the main thread of this process, and display its call tree.
Now we are able to analyze which calls might be expensive. We can see that this call tree is originated from a commit transaction, and it shows that we spent about 10 milliseconds inside a method called updateTags in our custom collection view cell. Let's look at what's in this app.
It is composed of a common collection view, and each cell shows: a photo thumbnail using a UI image view, some text using UI label, and some tags using a custom tag label view. Let's look at the implementation of our custom collection view cell class, and more particularly, where this method is being called. Here, we see that we have a property observer on the menu item.
This property observer will call update tags in two scenarios: a valid menu item was set, or it was set to nil. In the first scenario, we pass an array of tags that we want to display. In the second scenario, we pass an empty array to remove any remaining tags. Let's take a look inside the UpdateTags method implementation now. As expected, we remove all the views from the view hierarchy in case of an empty array of tags. Otherwise, we create a stack view if needed.
Then, recreate or reuse existing tag labels for each tag. After that, we remove any existing unused tag labels in case that the previous usage of this cell had more tags than the new one. Let's go back to our calling scope and let's look at the potential problem now that we have a better knowledge of how things are implemented.
The customCollectionView cell overrides the prepareForReuse method that is called when a cell is dequeued for reuse. And inside this method, we set the menu item to nil. Doing so will cause our second scenario to happen which removes all the previous tag labels from the cellView hierarchy without taking advantage of our reuse implementation logic.
This means that for every dequeued cell, we will remove all the previous label subviews, and reinstantiate every single view needed to display our labels. This is not optimal, and this might be causing the hitch. The solution for this is quite simple. We don't need to clear our menu item, and we can simply remove the prepareForUse method from the implementation. Now, when we set the new cells menu item, we can take advantage of the reusable logic and avoid expensive view hierarchy operations.
If we record a new trace after our fix, we notice that we drastically improved the number of hitches detected compared to the first trace. The time profiler in Instruments was very helpful for finding what code was taking too long and causing a hitch. To find out more about time profiler, watch the Using Time Profiler in Instruments video. After learning how to profile our application with Instruments, let's discuss some recommendations to avoid hitches in the commit phase.
Rule number one is to keep your views lightweight. To do so, try to take advantage as much as you can from the properties available on CLayer that are GPU accelerated and avoid CPU custom drawing. In case that it is justified, make sure to measure the performance of it.
Avoid any empty implementations of drawrect as the system must do extra work which will require additional time and memory usage during the commit transaction. Try to reuse views as much as possible to avoid expensive view hierarchy operations as adding and removing. Related to removing views, try to take advantage of the view property hidden if you need to stop showing a specific view during an animation. This is a lot cheaper.
Rule number two is to reduce expensive and redundant layout. Try to only rely on set-needs layout when you need to update your layout. Layout if needed will expand the current transaction lifetime and can cause hitches. Most of the time, you can wait for the next run loop to update your layout. Try to use the minimum number of constraints to avoid increasing the complexity for solving this.
Finally, a view should only invalidate itself or its children, but not its siblings or its parent view. Otherwise, the view's layout will be recursively invalidated again. I recommend you to watch the two WWDC talks mentioned below to learn more details about performant layout and image and graphics best practices.
Now that you understand the commit transaction pipeline, you can avoid expensive commits. You can use the new animation hitches template in Instruments to detect and investigate hitches. You've learned some strategies for preventing commit hitches, such as: Ensure "Prepare for reuse" does not incur additional work Keep your view hierarchy shallow and lightweight Avoid expensive and redundant layouts Also, be sure to learn about the next phase of the render loop in the video: Demystify and eliminate hitches in the render phase. Thank you