Jacob’s Tech Tavern

Jacob’s Tech Tavern

High Performance Swift Apps

Seek and destroy the bottlenecks in your code

Jacob Bartlett's avatar
Jacob Bartlett
Mar 04, 2024

From The Vault™ this week is one of my favourite ever posts. It was certainly the most fun I’ve had building and writing something.

I’m taking you through my patented 3-step process to drastically improve the performance of your app. Along the way, we’ll find a few very interesting algorithm and concurrency puzzles along the way.

I am finally releasing the source to turn this into a code-along tutorial!

→ Get the before sample code.

→ Get the after sample code.

Two weeks ago, I released Check ‘em: The Based 2FA App.

Get my code on GitHub

The concept was pure: a two-factor authentication app which sends you a notification whenever a really great number shows up. Tap the notification to permanently add it to your collection.

The kind people of Reddit enjoyed my write-up — from idea, to proof-of-concept, to release — enough to propel me to #3 on r/programming.

Mankind is finally recognising my genius

Check ‘em calculates millions of 2FA codes into the future, processes each number to determine if it’s interesting, and schedules the interesting codes as push notifications — GETs.

v1.0 works pretty well, but today I’m going to focus on performance.


My app performance evaluation process has three steps:

  1. Test on a real device to identify user-facing problems.

  2. Profile the app using Instruments to identify bottlenecks.

  3. Implement code improvements using 1) and 2) as our guide.

This find-problems-first approach avoids bike-shedding, keeping the focus on the real bottlenecks. 

“Any improvements made anywhere besides the bottleneck are an illusion. Astonishing, but true! Any improvement made after the bottleneck is useless, because it will always remain starved, waiting for work from the bottleneck. And any improvements made before the bottleneck merely results in more inventory piling up at the bottleneck.”

― Gene Kim, The Phoenix Project

This approach helps me avoid wasting time on general code improvements that don’t move the needle for our users (so the singletons are here to stay — sorry not sorry).

Testing On-Device

Device — iPhone 15 Pro 
OS — iOS 17.2.1

Processing speed

The big, glaring performance problem is exactly where you’d expect: processing your numbers while computing 2FA codes. This processing runs every time you enter the app, make a change to your 2FA accounts, or update your choice of GETs. 

TOTP calculation itself is a non-trivial function, involving byte manipulation, string creation, and cryptographic operations. These codes are then checked for several kinds of interestingness, before scheduling push notifications for the most interesting codes.

This processing is pretty fast when a user has common GETs enabled, such as quads (e.g.004444), or palindromes (e.g. 123321). Check ’em runs until it has scheduled 64 notifications (the limit on iOS), so it doesn’t need to calculate far into the future when there are several codes found per day. 

If a user only wants to see rare GETs, such as quints (e.g. 055555) or near-counting sequences (e.g. 012340) the total processing time can exceed 10 seconds.

Processing time for rare and ultra-rare GETs

Rarity increases exponentially with each tier. With only ultra-rare GETs enabled, such as sexts (e.g. 666666) or counting sequences (e.g. 012345), my screamingly powerful A17 chip takes take over a minute. This increases even more if you only choose one or two ultra-rare options.

As expected, this is by far the biggest performance bottleneck.

Time-to-first code

Since the app is pretty simple, just 1500 lines of Swift, the surface area of potential performance problems is pretty constrained.

A less offensive issue rears its head each time you launch the app fresh — while the launch itself is lightning-fast, it takes a little while for the first 2FA codes to appear.

It takes about a second for  — — — to turn into a useable 123456

Since a user would, ideally, be using this app for 2FA codes in their day-to-day, having useful codes snap into existence instantly would improve the user experience for the functional use case of this app.

Profiling with Instruments

Now we’ve identified the two key user-facing performance issues, we can perform a detailed analysis with Xcode Instruments. This profiling will allow us to detect the exact function calls which bottleneck our code.

Setting up Instruments

If you’re new to performance profiling, Instruments is a separate app which can be opened from the Xcode Developer Tools menu. 

Instruments in the Xcode menu bar

Our bread-and-butter Instrument is the Time Profiler, which monitors CPU cores. It samples these cores 1000 times per second, recording the stack traces of the executing functions. The resulting report shows which parts of your code are hogging compute resources.

The Time Profiler is our bread-and-butter Instrument

Now we’re all set up, we can start our investigation.

Processing speed

Opening up our app, and monitoring the notification re-computation, we see some kind of async process taking up nearly 20 seconds as Check ‘em searches for interesting codes.

A messy set of call stacks in the initial report

To make this easier to comprehend, there are 3 settings we can toggle from the Call Tree menu: 

  • Separate by Thread splits the recorded processes up by thread rather than grouping all the same function calls together. This means if you have processes running in parallel, you see their time profiles separately.

  • Invert Call Tree reverses the measured backtrace. This means instead of displaying the low-level iOS system calls which run on the CPU, we can see our own functions at the top of the call stack. 

  • Hide System Libraries cleans the report up by removing system library processes, making your own code easy to spot.

Now, we can easily see all the processing work which is happening on a background thread as we expect. 

Cleaned up Time Profiler report showing slow processing on a background thread

Our TOTP generation uses a non-trivial, but very well-specified algorithm. If I’d invented a way to speed this up, I’d be writing this article on from somewhere hot, on a yacht. 

What I’m trying to say is, we’re not likely to find a shiny new algorithm to speed up this TOTP generation process. Therefore, we firstly want to improve anything slower than the OTP.init() method.

The smoking gun: the regex looking for sextuples

We’ve found the smoking gun. The slowest calculation by far is checkThoseSexts(), which wraps a regex that appears to be ridiculously computationally expensive. 

extension String {
    func checkThoseSexts() -> Bool {
        (try? /(\d)\1\1\1\1\1/.firstMatch(in: self)) != nil
    }
}

When I also check for quints and quads, which use similar regexes, they also substantially outweigh the TOTP calculations.

Since quads are so common, the full processing was terminated in just over a second.

We’ve got a pretty clear indication that swapping out the regexes for alternative approaches could approximately double performance. 

This analysis helped me realise something further: this heavy processing is running serially on a single background thread, with pretty much all the heavy work offloaded to _dispatch_workloop_worker_thread, created by the Swift Runtime from my high-priority detached Task. 

This means just a single CPU core is responsible for generating TOTPs and checking their interestingness. If we’re clever about it, this process is dying to be parallelised.

Time to first code

The other user-facing problem is the relatively slow time between opening the app and seeing a useful 2FA code displayed.

Since we know exactly what start and end points in our code to look for — app launch up to the first displayed code — we can profile this problem with a blunter tool: print() statements and timestamps.

07:35:54.2640 - App init
07:35:56.0280 - Code displayed

The app currently takes a massive 1.764s between initialising the app to seeing a useful 2FA code. Following the code paths from launch to code generation, we get a clearer picture:

07:50:10.5640 App init
07:50:11.1980 On appear
07:50:11.2290 Set accounts
07:50:12.1630 Refresh accounts
07:50:12.1640 Code displayed

This shows two relatively long waits:

  • The time between app initialisation and onAppear firing on the main view.

  • The time between setting Accounts in the view model and refreshing them to get TOTP codes. 

The keychain operation to fetch accounts, and the subsequent code generation step, are actually pretty quick, so don’t substantially impact the total time. 

Now that we’ve identified the key performance bottlenecks, we finally can start to make some targeted code improvements.

Code Improvements 

Our basic run-through of Check ‘em found two performance issues:

  1. Processing millions of TOTPs and checking for interestingness.

  2. Slow loading of the initial 2FA codes. 

Based on our detailed analysis, we identified 3 specific improvements we can make: 

  1. Replace the regexes used to assess interestingness with more efficient algorithms.

  2. Introduce parallelism to the TOTP and interestingness calculations. 

  3. Make the initial code generation run sooner after launch.

Let’s see how much speed we can squeeze out of this app!

Subscribe for free to continue reading

Enter your email and get every post from Jacob’s Tech Tavern.
Already have an account? Sign in
© 2026 Jacob Bartlett · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture