Our 1 Mistake in iOS App Modularization That Slows our Xcode Build process

Agung Pratama
Stockbit-Bibit Engineering
8 min readFeb 7, 2023

This was never mentioned in the usual “How to make Xcode build faster” articles, and it might be happening to your codebase too.

Photo by Chris Peeters from Pexels: https://www.pexels.com/photo/shallow-focus-photography-of-snail-12832/

So half a year ago, I upgraded my work laptop from a regular M1 chip Macbook Pro to an M1 Pro chip Macbook Pro. Feeling enthusiastic, the first thing I did after setting up the nitty-gritty was compile my work codebase from a clean state and expect a certain performance jump to build the app faster. What happened was… I was shocked that the difference was not as big as I expected. 🤔

State of the App

Working in a technical-focused path in Stockbit for almost a year, I have a personal mission to make the iOS codebase scalable and enhance the developer experience. Therefore optimizing Xcode build time is always a never-ending thing to do. We had done a lot of research and had done most of the things those articles on “How to make Xcode build Faster” mentioned. We were confident that no huge or fatal mistakes slowed down Xcode compile time in our build pipeline. Most of the things we found had slowed us down, especially in swift compilation time, were already optimized in the past, … or so we thought.

Looking for the culprit

Unsatisfied and disappointed with the hardware, my first instinct was to check the MacOS activity monitor and see my CPU load.

CPU shows dropped of load in the middle of the build time

🤔 hmm? So this is why my new hardware doesn’t show expected differences? This was not a very good sign. The dropped CPU load midway shows that my new hardware was not working as hard as it could, which is unfortunate.

Googling around, I found that Apple recently shipped the new Xcode 14 built-in Build time Timeline Visualization tool. By using it, I found a more shocking finding.

Xcode 14 Timeline Visualization, Notice the extensive red line in the middle of the timeline

This can be accessed via Report Navigator > Local > and highlight one of the build processes, right click and “Show in Timeline.”

What does the timeline visualization tell us? 🤔

The current modern Xcode build processes utilize modularization and parallelism in the build process. The horizontal axis from left to right shows a timeline period, while the vertical axis shows a parallelism stack that shows which process is being handled with each CPU thread.

This means the big gap red line in the middle shows us that a big singular process took 1/3 of the build time and bottlenecked other further build processes. 😱 Which is… Compiling Assets in our UI module. This singular process won’t take all of your CPU fully loaded, yet there are no other processes that can be processed simultaneously at the same time. And the worst of all, this process is so extreme, being the largest singular process around 3 times longer than the second largest process in the whole build.

Why is this happening?

Since the start of our journey of modularizing the codebase, there was a decision taken at the time to put UI-related things in a singular module instead of each business unit framework. This was because many of the UI and assets are shared between business frameworks, and to make it “convenience” to be used, we placed it in a lower-level module (UI framework). But unfortunately, our UI Framework is in a higher hierarchy than the other low-level modules like Utility, Networking, or Core modules, since the UIFramework actually still needs all of them as its’ dependencies.

Here is an oversimplification diagram of how the culprit UI Framework is placed in our dependency hierarchy.

Where the UIFramework stands, oversimplified. Being a bottlenecking process from the low-level modules (green) to the higher-level business modules (yellow)

In other words, all of our low levels are not having a horizontal relationship and have UIFramework as a single module that bottlenecks. Combined with the “convenience” part of just throwing assets (which most of them actually belong to other business modules) in that particular UIFramework module. In the end, this hurts our build time optimization process.

How to Prove?

Okay, we have our biggest suspects right now. We can throw a bunch of theories and blame the UIFramework as our black sheep all day. Can we prove it?

Anyone who has worked on a modularized iOS codebase must know that managing assets is one of the trickiest things to solve in iOS modularization, a necessary helper function to load UIImages from cross modules, especially the limitation of XIB that can’t really load imagesets and colorsets from cross modules and such. While all of this is still just a new finding, putting all of the imagesets back to where it belongs in the higher-level business modules is always a huge effort full of risk of a crash or failure to load assets. So before doing anything further, we have to be very sure that auditing and moving all of the assets are worth doing.

So we have come up with our sets of experiments:

#1 Simplest things to do. let’s delete the whole .xcassets in the UIFramework and rebuild.

Our first witness is the result of monitoring the CPU load via the same Activity Monitor. After deleting the whole xcassets in the UIFramework module, my machine CPU load stayed full the whole build process from the start until it finished.

after deleting the suspect completely, my CPU load stayed full the whole time

Then we go back to the visualization build timeline process. This time we don’t see the big gap line in the middle; all of the parallelism processes are dense enough to be shown in the timeline chart.

The big gap line is gone. And all of the build processes now look dense.

Well, since we blatantly deleted the assets, obviously the build time would be way faster since essentially we just removing a lot of processes to do.

Still, we can see for a fact that the CPU loads from the activity monitor and the timeline visualization show that we are now using all of the machine CPU loads fully, unlike from the original build state.

#2 Randomly moves out 70% of assets from the FrameworkUI to the BusinessA module.

After experiment #1 shows some promising improvement, we audited the whole image sets that around 70% of the assets placed in the UIFrameworks can be moved to various higher business modules. So this time, instead of deleting the assets blatantly, I tried to randomly pick around 70% of the assets to be moved in a single higher business module framework.

Which shows something like this:

70% of random assets were moved to a single existing framework

There is still a gap in the timeline, which shows a bottleneck issue in the process. But this is way better, and even with the same amount of assets (nothing is deleted) our build time is already around 20–25% faster! This proves another promising thing on why this needs to be addressed and is worth doing.

There is a new big green line in the timeline in which our previous 70% of assets from UIFramework were moved. Yet the other build processes are still jam-packed and dense. The CPU load also shows no further drop in this period, even though an extremely long process is still happening. Why? Because of the horizontal relationship between all of the higher modules! We already fix our previous problem with our business modules so it is all independent of each other, even if there is one single particular process that would take much time just as shown in the previous visualization, it won’t hinder all of the remaining processes and have 0 bottlenecks!

Our plan to solve this

With all proofs presented, It is clear now that this is a problem worth doing for our codebase improvement. Our plan right now is trying to trim out all of the excess assets placed in the UIFramework and audit it if it can be moved out or more properly if it is placed on a higher-level feature module.

Obviously, with a codebase that is now surely big enough to have assets compile time bottleneck problems, moving assets out needs to be audited and tested carefully. We are currently taking our time to handle this problem as carefully as we can and plan to do things in many separated steps in many sprint iterations. If your codebase has this problem as well, we encourage you to do the same.

Having a faster build and fixing the dependency problem is great but ensuring there are no missing images that might cause bad experiences for your user is a non-debate priority.

Conclusion of the findings

  • Sometimes upgrading your existing machine to a more modern, newer, and more expensive machine just won’t cut it. So you might want to check your code base build process first before requesting the new M2 Pro chipset from your manager.
  • Always check your actual build process. Period. This new Xcode 14 has a new built-in visualization feature that you might want to check on. Who knows, this could be a surprise to you as well.
  • You might have already done a lot of things that are listed in many other medium articles on “How to speed up Xcode build process” but sometimes the problem is more specific towards your own organization codebase or even an architectural level like how it happened in this case.
  • Always try to make your modularization layering as horizontal as possible on each layer. I cannot stress this enough. Sometimes when there is an inevitable singular process that took a long time, it won’t bottleneck all of the other build processes that are placed on the same layer.

--

--