Skip to content

Start Up Improvements

Greg Bolsinga edited this page Jan 7, 2026 · 10 revisions

Focus on Memory Use

I want to be able to cache the calculated data (ie various rankings, sort tokens, processed Music data). Then this data will be able to be used right on launch. Then if the downloaded data is newer, new data would be re-calculated.

At first I decided I wanted to make this data smaller. The Digests in the code had some redundancy, I only wanted them to have what was necessary to display the information, and link to what it needed to link to. Originally I created them so that views would not have to look things up on display. They wound up having more data than necessary. So I pruned them down to what was required.

All this data is computed at launch, right after the full set of data is pulled from the server. This application is very "optimistic". It will get all of the complete shows data from the network, convert it, and preprocess some data for faster sorting and ranking at runtime. It doesn't "care" if it takes long. It was a choice to be very simple versus being complicated to speed things up. All of this work is done before the UI is shown, while the first spinner happens. It's been pretty fast (even from the first version).

A programmer is never satisfied, and I wanted to look into how I could improve it.

Use The Tools!

So of course, in classic programmer style, I first just tried doing things to make the structured data look smaller by intuition. This worked for awhile.

However I wanted to really measure. So I used Apple's terrific tool, Instruments. I decided to use macOS, since the launch cycle is fastest there (no syncing the program from the mac to the phone or simulator required). I have a command line program that will read and write the data to the console and then quit. I thought I'd try this out as a simple path. However none of the code symbols showed up in Instruments this way. I dug around and was unable to find a quick solution.

So I made a local edit to the program so that it would exit(0) right after creating the processed data. This way the program would always stop in the same place, the symbols for the code were visible to the tool, all the data would be processed, and nothing else would be able to affect the measurements.

In Instruments, I used the memory allocation tool. In particular, I wanted to watch the memory that was "Created & Destroyed". This gives insight into work that is done, but then thrown away.

Use count(where:) Not filter{}.count

Then I learned something I didn't expect. I saw lots of arrays of Show that were being thrown away! I was surprised to see this. It was about 25 MB of transient data! Instruments allows you to examine the call stack (sort of like the step-by-step directions) to where this was occurring. I looked and it was code that counts how many artists were at a venue, how many shows at a venue, etc. This code was taking an array, filtering it on a condition, and calling .count. The logic seems straight forward. However that .filter creates a new array, counts the items, and tosses the array it created. I looked and found out about count(where:) and the array is no longer copied. I can't remember how much memory it saved, but the fix helped!

Use compactMap{}.count with conditionals returning nil instead of filter{}.map{}.count

There were still more copies of Array<Show> going on, and my code had more .filters to examine. The next culprit was code that .filters the Show to count the span of years an artist was at a venue (for example). My solution here was to use .compactMap.

Re-use data created on heap for frequently used code

The next one I wasn't expecting was when getting DateComponents from a Show's date. This code path executed often and the API requires a Set<Calendar.Component>. So there were lots of these created and destroyed on launch. My solution was to create private let constants with the required Sets. Transient memory went down by 10 MB!

Don't do extra work

The last large chunk of transient memory I found before writing this had to do with Regex. On launch the application will make names simpler to sort as they would be sorted by a library. It removes leading punctuation and words like "the". This way "The Sea And Cake" will sort under "S", for example. It also was used to quickly build a map of the sections names should be under, for quick lookup at runtime.

I actually recognized this problem while creating the Regex code. I wanted to build a struct that would re-use a Regex instead of re-building one each call. At the time, I couldn't figure out the swift syntax for this (Regex uses a builder pattern and lots of generics). Looking at this with the tool I realized one simpler fix that could be done with the same syntax, and reduce the work by half. I realized that every name in the data was run through the Regex twice! I fixed that by doing the work once up front, and sending this data to the two places that require it. That alone saved 40 MB of transient memory!

Re-use Regex

Then I spent some time to learn how to have the Regex as a property. Then this struct would have a method that would re-use the saved Regex for the library sort tokenizer. This fix reduced the Regex transient memory by around 30 MB to just about nothing. I figured how to do this by using Xcode's Option-Click on a symbol to learn what its type is. I then used that type as the property type, and then iteratively reduced it as much as possible.

Results

I modified a copy of the code before this memory reduction work and after, both variants exiting the program after the data has been created. I ran it 5 times for each mode. Here are the results for this small amount of work done. I selected "All Heap & Anonymous VM" in Instruments, and put the data into a Numbers sheet.

Persistent Bytes (MiB) # Persistent # Transient Total Bytes (MiB) # Total Execution Time
Pre Average 26.868 71302.6 878615.2 189.446 949917.8 2.0348
Post Average 24.842 60841.8 249181.4 107.944 310023.2 1.4324
Reduction 7.54% 14.67% 71.64% 43.02% 67.36% 29.60%

These are great results, and I didn't even look at "hot" code. I just looked at allocations.