This stack trace is from a pretty straightforward sample app that renders some images and text in a feed.

The panel on the right shows the heaviest stack trace. If you look at the line I've hightlighted, you'll see that there's a function called applejpeg_decode_image_all that's being called. If you follow the stack trace up you'll also notice some copy functions.

The tricky part about this being a big bottleneck in your app is that literally none of the code in the stack is code you wrote directly. If you turn off "System Libraries" in the Call Tree options, this code won't show up at all.

Even if you aren't hiding system libraries, it can be easy to accidentally ignore Apple code in traces since, let's be honest, it can be a little intimidating to look at and it's easy to subconsciously try to find code we're already familiar with.

To counter this tendency, let's take a quick detour and go over what's happening in this stack trace at a high level.

Core Animation Overview

We've talked about how the UI is rendered via the Render Server before, but let's go into a little more detail.

Anything that happens to a CALayer (or UIView by extension) goes through a pipeline of 5 stages to get from the code you've written to showing something onscreen.

Under the hood, a CATransaction will be created with your layer hierarchy and committed to the Render Server via IPC. On the Render Server, the CATransaction will be decoded and the layer hierarchy re-created. Next, the Render Server will issue draw calls with Metal or OpenGL depending on the phone. Then, the GPU will do the actual rendering once the necessary resources are available. If the render work is done before the next vsync, the rendered buffer will be swapped into the frame buffer and shown to the user.

An important thing to take note of is the fact that number 1, aka the CATransaction phase, is the only phase that runs in-process. Everything else happens on the render server!

This means it's the only phase that you can see in your stack traces as well as the only one you can directly influence.

CATransaction Overview

The transaction phase itself is broken down further into 4 stages (I know, too many stages to remember right?). The first two should be pretty familiar to you already.

Layout: Do all the calculations necessary to figure out layer frames. -layoutSubviews overrides are called at this point. Display: Do any necessary custom drawing via CoreGraphics. -drawRect: overrides are called and, interestingly, all string drawing happens here aka UILabels and UITextViews. Prepare: Additional Core Animation work happens here including image decoding when necessary. Commit: Finally, the layer tree is packaged up and sent to the render server. This happens recursively and can be expensive for complex hierarchies.

Now that we have the high level overview of what Core Animation will do, let's take a look at that stack trace again.

Starting from the top, we see that main() kicks off the run loop by calling [UIApplication run]. Then, in the "do observers" phase of the run loop run we have a CATransaction being committed with the CA::Transaction::commit() function. I've highlighted the section that corresponds to this commit.