How one line of code led to +50% faster Swift compilation for tens of thousands of teams

Ruslan A.
The Qonto Way
Published in
11 min readApr 19, 2024
Can one line of code change the iOS development industry?

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:

  1. Automating mock generation.
  2. 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.

Sample code of swifttemplate file contents, depicting its similarity with swift source code files
Example of swifttemplate file contents

How does Sourcery parse swifttemplate files?

When swifttemplate is provided to Sourcery, along with its source code files, this is what happens:

  1. Sourcery strips the given swifttemplate off of any Sourcery-specific tokens.
  2. Sourcery generates a specifically crafted ephemeral Swift package.
  3. Sourcery compiles the generated ephemeral Swift package into an executable binary file.
  4. 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.

Diagram depicting the sequence of actions Sourcery library performs when generating mocks. It starts with scanning sources, then compiles an ephemeral executable, finishing with mock generation
A diagram showing how Sourcery generates mocks based on 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.

Code sample showcasing how to execute Sourcery concurrently with the Swift modern concurrency model using withThrowingTaskGroup(of:) method
Code which shows how we ran Sourcery concurrently using the modern Swift concurrency

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%:

A diagram depicting measured compilation duration of each type in SwiftTemplate ephemeral package. Before the initial optimization, the total compilation duration was 672 ms. Afterwards, it became 610 ms.
Compilation duration of types in SwiftTemplate before and after the initial optimization attempt

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 SwiftTemplatethese optimizations are unnecessary. Realizing this, I immediately disabled code optimization when SwiftTemplateephemeral 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.

Screenshot from GitHub Actions, where before this optimization, duration of a pipeline was 26 minutes and 16 seconds. After the optimization the duration became just 8 minutes and 43 seconds.
Comparison of unit test bundle execution duration from GitHub CI

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:

Screenshot of the new argument passed to swift compiler, which disables any optimization performed while SwiftTemplate ephemeral package is being compiled
Disabling optimization during compilation of SwiftTemplate executable

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.

Diagram depicting the result of running Sourcery before optimization, which was 72 seconds, and after this optimization, which now takes 65 seconds.
Comparison of mock generation duration before and after the second optimization attempt

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.

Screenshot which shows Instruments app with measurements of Sourcery execution and with highlight of the slowest part of its code.
Screenshot of the profiling process

Profiling did not answer the question specifically, but pointed me in the right direction: processing of the swifttemplatefrom 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.

Diagram showing the decision tree Sourcery uses to either use the cashed SwiftTemplate binary, or compile a new one.
SwiftTemplate cache and reuse mechanism

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.swifttemplatefile, 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:

  1. 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.
  2. 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:

Table with different configurations of Sourcery and swifttemplate with duration in seconds for every type of configuration.
Table showing each configuration of running Sourcery, with each configuration’s duration shown in seconds

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!

Diagram showing the measurements made before and after all of the optimizations mentioned in this article, bringing 74% improvement overall.

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

--

--