How one line of code led to +50% faster Swift compilation for tens of thousands of teams
Introduction
How can one line of code change the iOS development industry for the better?
That was the question I asked myself recently, when I finished optimizing Sourcery, SwiftyMocky, and tooling in Qonto’s iOS project. And it’s a question that’s worth asking — just one engineer working on one open source project has the potential to have a huge, positive impact on many people.
How it started: one year ago
Working in a large engineering team at Qonto, we’ve got clearly established processes, best practices and state-of-the art tools. A plethora of these tools are built using the Swift programming language — and one of these, related specifically to iOS development, is the Sourcery library built by Krzysztof Zabłocki.
At Qonto, product quality is the first priority for all teams. To drive quality, Qonto’s iOS team mostly uses mocks for unit testing, which facilitate the infrastructure of more than 15,000 tests.
Last summer (2023), we optimized mock generation by splitting a large Mock.generated.swift
file. Our engineers loved this upgrade, sharing positive feedback:
Interacting with mocks has improved my happiness as a developer…I can’t imagine that, a few months ago, I would have to wait for more than one minute to open a file.
Radu Dan, Senior iOS Engineer @ Qonto
When I joined Qonto 3 years ago, we were still using Macs on Intel CPUs. Whenever I happened to accidentally ‘open file’ with all mocks, I used to force-quit Xcode, rename the file on disk (otherwise it would auto-open it on next launch), launch Xcode, and bring back the old name. It was still much faster than waiting for Xcode to actually open the file.
Mariusz Wiśniewski, Senior Staff iOS Engineer @ Qonto
But we weren’t stopping there. The Qonto iOS team concentrated on further tooling improvements to support our fast-growing team.
There were both obvious and less-obvious bottlenecks in scripting — in particular, two problems to solve, which were affecting both the developer experience and the CI runtime:
- Automating mock generation.
- Optimizing execution of tools related to mocks.
So, the make mocks
command was introduced, which runs Sourcery and uses SwiftyMocky’s swifttemplate
, and - as of now - generates 3,189 mocks for more than 50 modules in iOS project.
1. Automating mock generation
Mock generation automation had a certain set of benefits, mostly targeted at improving developer experience and build system consistency. After the mock generation part was moved out of the project build phase, the team had a new workflow to follow. Due to this, after rebasing or switching git branches, developers often needed to execute a set of commands in the terminal in order to update mocks.
The automation was implemented using git hooks. That is, during checking out a branch in git, commands would be executed. It meant that if mock generation took longer as the codebase grew, switching branches became noticeably slower. For this, a smart caching was implemented, which enriched an existing Sourcery built-in caching system. Only those protocols which were modified would be marked for the new mock generation procedure and processed via Sourcery. Sourcery would not be launched if no protocols were modified between executions.
Such optimization made it possible to reduce the duration of mock generation to 10 seconds in the worst case.
But waiting 10 seconds multiple times adds up — and when there were no cached generated mocks available, it could take up to 50 seconds to generate thousands of mocks for a heavy protocol-based Swift codebase.
So, I started optimizing the internals of make mocks
, unknowingly taking my first steps into a year-long battle of optimizing every single bit of code I could in Sourcery, SwiftyMocky, and make mocks
-related internal tools.
2. Optimizing the execution of mocks creation
It takes 70 seconds to generate 3,189 mocked protocols. But not only that — generation is made such as that there’s 1 file per 1 protocol, which is much slower than if we use a single file for all mocked protocols. The main issue with this approach is that it’s not scalable, if we consider the never-ending growing number of protocols to be mocked in a codebase, where features get merged dozens of times every day.
To address the scalability of both SwiftyMocky template and Sourcery, let’s dive into the technical details of how exactly Sourcery works when processing swifttemplate
files:
What is a swifttemplate
file?
Sourcery supports multiple templating languages, which guide the code generation process. These formats are: stencil
, ejs
, and swifttemplate
. As mentioned, SwiftyMocky provides a built-in Mock.swifttemplate
file, which contains a vast amount of internal code for a sophisticated protocol mocking, and provides a number of functions to stub and verify different versions of business logic when writing tests.
At its core, swifttemplate files look very similar to Swift source code files. There are slight differences related to Sourcery. After processing the template, Sourcery generates the expected mocks accordingly.
How does Sourcery parse swifttemplate
files?
When swifttemplate
is provided to Sourcery, along with its source code files, this is what happens:
- Sourcery strips the given
swifttemplate
off of any Sourcery-specific tokens. - Sourcery generates a specifically crafted ephemeral Swift package.
- Sourcery compiles the generated ephemeral Swift package into an executable binary file.
- Sourcery passes the scanned sources to the compiled ephemeral executable binary as an argument.
The resulting executable binary file, named SwiftTemplate
, contains only the processed swifttemplate
and all needed type definitions related to processing of the annotated mockable types. These are protocols for which mocks would be created during the execution of SwiftTemplate
.
This may sound complicated — it’s a very sophisticated setup, with its own specific challenges, which we’ll explore further later. But first, let’s go back to June 30, 2023, when I made my first PR to the Sourcery GitHub repository.
The open source side of the story
When writing the initial version of scripts for mock generation, I came across a limitation in Sourcery. This limitation didn’t allow Sourcery to be included as a Swift Package from its public APIs to generate mocks within another script. To resolve this, I first copied Sourcery locally and changed it for my needs to try and execute code generation using async/await
. The Swift concurrency model was as shown below.
My PR was accepted and merged within just 24 hours. But there was a catch: Sourcery did not support concurrent execution at that time!
Not knowing enough about the intrinsics of the process, I didn’t have a clear idea of how to resolve this. So I reverted to using Sourcery synchronously, iterating through 50 modules and generating mocks for each mockable protocol.
Paying attention to how the Sourcery codebase was organized, and offering a number of contributions and bug fixes, on July 4th, 2023 I created a new issue. In this, I asked the author of the library if there was anything that could be parallelized in regards to a specific part of tests.
The response was that this wasn’t a particularly important focus for test-driven development, as the code in question was an integration of Sourcery execution workflow.
But I wasn’t satisfied with this.
As a result of this experience, I kept in the back of my mind that Sourcery execution had a tendency to run slow. I had a gut feeling that this speed might be improved somehow, though I had no idea how exactly to do it at that time.
Fast forward to March 2024. Having spent 8 months working closely on Sourcery codebase, I was able to resolve this exact issue! Read on to find out how.
Internals of Sourcery processing — SwiftTemplate
When Sourcery is being executed, it scans the given source files and searches for all relevant annotations. After the scan process is finished, collected metadata is then passed to the SwiftTemplate
binary executable mentioned earlier. There, according to the given swifttemplate
, processing is done and the final generated code is printed into a file as a result of the last step of running Sourcery.
Within all these steps, there is one step in particular which caught my eye when I was trying to improve the runtime performance of swifttemplate
processing.
Speeding up swifttemplate
processing
My initial approach for speeding up swifttemplate
processing was to measure the Swift compiler performance when processing types using SwiftCompilationTimingParser library. It’s the same technique I used when resolving Swift compiler issues at scale, working on one of the most important parts of the Swift language ecosystem, SwiftSyntax open source library.
By changing Swift code for a better compilation duration, I was able to achieve a very slight improvement in SwiftTemplate
compilation time, which is a part of Sourcery execution time, by 2%:
And while this initial improvement was small, a more significant one was just around the corner.
When I was working in this area of code in Sourcery, I had noticed that the compilation of SwiftTemplate
was using an optimization. That is, the compiler was instructed to optimize the code for faster runtime performance, or smaller size of the final binary. While these optimizations are crucial for, say, codebase with intensive calculations, for SwiftTemplate
these optimizations are unnecessary. Realizing this, I immediately disabled code optimization when SwiftTemplate
ephemeral package is compiled. The results were impressive.
The compilation time of swifttemplate
files by Sourcery was reduced by ~57% - directly related to the issue I had reported initially, where integration tests of SwiftTemplate were running slowly.
I was very happy with the result, not only because it was such a big improvement, but also because it was achieved through changing one single line of code:
This change could play a crucial role for tens of thousands of companies and teams using Sourcery and swifttemplate
files, including Qonto.
Nevertheless, when I was testing the new version of Sourcery library with mock generation tools, I was shocked by how little this improved for the Qonto iOS project setup — just ~10% improvement overall.
The next step of the investigation started: why wasn’t mock generation improved by this change? I tried profiling Sourcery runtime with Instruments, bundled with Xcode IDE.
Profiling did not answer the question specifically, but pointed me in the right direction: processing of the swifttemplate
from SwiftyMocky
was taking most of the time, even though it was not traceable directly during profiling.
Sourcery supports its internal caching mechanism!
As it appears, after going through SwiftTemplate
processing, compilation, and execution, I could see that Sourcery does have a built-in caching mechanism of the SwiftTemplate
executable itself.
However, using the built-in cache as-is, not only did not speed up the execution of generating mocks, but instead would sometimes slow it down by ~25%. Something needed to be changed elsewhere.
Concurrency
As SwiftyMocky is open source, I was able to study the codebase and dove into optimization. This time, I did not measure compilation duration. Instead I studied all types of iteration, from serial to concurrent, using the async/await
Swift concurrency model.
Applying the modern Swift concurrency to the Mock.swifttemplate
file, I was able to introduce concurrent walkthroughs of all of the scanned types, seemingly boosting the SwiftTemplate
execution speed considerably. Then, I needed to run a test, but hit another bottleneck: Sourcery did not support concurrency in swifttemplate
. This was simple to fix though, and, as of Sourcery release 2.2.3, concurrency is supported in swifttemplate
files.
Combined with the code in mock generating scripts, which we had attempted to use back in June 2023 to run multiple Sourcery processes in parallel:
- Mock generation is now scalable — as the number of protocols grows, with modern hardware it’s possible to parallelize the processing and mock generation efficiently.
- Mock generation is now quick — using git hooks and switching between git branches is faster than ever!
By carrying out open source improvements both in Sourcery and SwiftyMocky, I was able to improve all other projects which rely on these tools — while also providing the new concurrent way of writing swifttemplate
to all consumers of these libraries! It was a win for all users.
Impact
After trying all configuration combinations when generating mocks, the following results were recorded:
As shown on the table above, the fastest combination of Sourcery and swifttemplate
configurations is swifttemplate
with concurrency, and Sourcery run in parallel. This version is running without caches for 34.81 seconds, and with caches within just 16.76 seconds. That’s 74% faster than before any optimization!
The upshot is: when using Sourcery, use async/await
in swifttemplate
and execute Sourcery
in parallel for all modules in the project to get the fastest, scalable system of mock generation.
Conclusion
Transforming a simple hobby — contributing to open source projects — into a tool that works for me as a way to achieve a sense of zen has had many additional benefits. It’s allowed me to make drastic optimizations on a scale of thousands of companies, and it’s improved our tooling at Qonto in the iOS project, speeding up the day-to-day workflows for each developer and reducing CI cost.
Giving back to the community is always a significant gesture that many companies postpone, solving their own problems first. However, especially in software development, teams are interconnected on many levels; so postponing the process of pushing the changes upstream to an open source project is likely to cost more as time passes.
At Qonto, we have established an open source standard, which provides concrete guidelines on how to publish an open source repository, and how to make a contribution to an existing one. By having such guidelines, the amount of hesitation, doubt, and friction is significantly reduced. And the result? Every developer at Qonto is encouraged to seek and adopt the smoothest ways of working, using existing instructions and best practices.
About Qonto
Qonto makes it easy for SMEs and freelancers to manage day-to-day banking, thanks to an online business account that’s stacked with invoicing, bookkeeping and spend management tools.
Created in 2016 by Alexandre Prot and Steve Anavi, Qonto now operates in 4 European markets (France, Germany, Italy, and Spain) serving 450,000 customers, and employing more than 1,400 people.
Since its creation, Qonto has raised €622 million from well-established investors. Qonto is one of France’s most highly valued scale-ups and has been listed in the Next40 index, bringing together future global tech leaders, since 2021.
Interested in joining a challenging and game-changing company? Take a look at our job offers.
Illustration by Pierre-Alain Dubois