Last Week on My Mac: Leaking like a sieve
In the last week, I’ve published details of two memory leaks, here and here, affecting the Finder in macOS Sonoma. Although they’re not the most severe type of bug that can affect macOS, they’re normally considered to be serious, and to warrant urgent remediation. This article outlines how memory leaks occur, and why they are so severe. To avoid complicating this further, I here refer only to leaks in app memory, and not system memory in Mach zones; a memory leak there can easily result in a kernel panic.
How to leak memory
The fundamental error that causes a memory leak is the allocation of memory that isn’t released once it’s no longer required. This is easier to do in some programming languages than others, but my examples here are in Swift, as it’s generally fairly understandable even for those who don’t code.
Each time that code uses data, that data has to be allocated memory for its storage. At its most basic, this applies to all the data used by code, but small amounts, for integers and floating point numbers for example, are allocated within app working memory and freed automatically once those constants or variables are no longer within ‘scope’ of the code.
More substantial amounts of memory may be handled automatically, but often require explicit allocation and deallocation. My two examples are taken from my own apps, the first from Fintch/Dintch/cintch, which calculate the hash of files to tag them and check their integrity. To do that, they provide a buffer into which file data is read, using the code
let theBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: theBufferSize)
which creates a buffer named theBuffer with the capacity theBufferSize. That buffer is then passed to the function that calculates its hash, and so on through each file. The clue here comes from the explicit call to allocate that buffer, so once it has been used it must be released using the call
theBuffer.deallocate()
to free up the memory that it used. If the engineer omits that release, then every time theBuffer is allocated, that memory leaks. The app loses track of it, doesn’t free it automatically (as it must be explicitly deallocated), so it can’t be freed. In an early version of Fintch, that’s exactly what happened, until I realised my mistake and explicitly deallocated the memory.
In the last few days, I had another app, which I use to test CPU cores in Apple silicon Macs, that had an even worse memory leak. I had added a new test, involving sparse matrices, based on some sample code provided by Apple. Unfortunately, that code wasn’t complete and didn’t explain that the sparse matrices I was using also required to be explicitly deallocated using the call SparseCleanup().
In this case, the leak was catastrophically large, as each time I used this new feature, at least one million sparse matrices were allocated memory that was never freed using SparseCleanup(). Every time I ran that particular test, the app used another 1 GB of memory in less than 5 seconds.
The only grey area here is in retention of allocated memory. In some Finder views many QuickLook thumbnails are fetched to display in that window. Clearly, having to fetch them all every time a user scrolls through the window isn’t sensible, but there’s no point in retaining them when the window is changed to List view, or closed altogether. If the only way the user can free working memory like that is to quit the app, then you’re designing it wrong.
What does a memory leak do?
macOS manages memory very well; it’s had over 20 years experience after all. What it can’t do, though, is stop a memory leak in an app. It’s unable to tell whether that memory is really needed by the app, or just going to waste, so the app’s memory will grow unchecked. Once it gets to a large enough size, macOS has to start using virtual memory stored on disk. On Apple silicon Macs, with their fast CPUs and internal SSDs, the user may not notice when that happens, but most Intel Macs will slow noticeably.
Switching between apps may then require noticeable time as memory is written to and read from the backing store, and the user starts seeing the spinning beachball. Eventually, the whole Mac grinds to a halt as more and more memory is devoted to containing the leaks.
How can you mitigate against a memory leak?
The simplest answer is not to use that app, or to avoid using the feature that causes the leak. We don’t have the first of those options with the Finder, because of its singular importance in macOS. The second may be fine for some leaks, but in the case of these two Finder memory leaks, there isn’t a suitable alternative in macOS.
These leaks affect two of the Finder’s four view types: Icon and Gallery view. The other two, Column and List view, don’t support the browsing of many QuickLook thumbnails within the one window, for example when looking through many images quickly. Alternatives such as QuickLook Preview simply don’t offer the same facility, and the closest that I know of is the Browser in GraphicConverter, a third-party paid app.
For those with Macs with ample physical memory, a memory leak may be tolerable for a limited period. Neither of the Finder leaks are in the same league as my app that leaked 1 GB every time you ran a test. But trying to do your work with one eye watching Activity Monitor and periodically restarting the Finder, then finding your place in your working window again, isn’t a good way forward. For those whose Macs are already a little tight for physical memory, this simply isn’t workable.
How to fix a memory leak
Many memory leaks are obvious, and can be found by identifying where the leak is occurring, and reading the code and documentation for its API calls carefully.
Some are far more obscure, particularly where the code has a lot of history, and has been changed by many people over the years. Fortunately, all code that’s developed within Apple’s Xcode has the benefit of its Instruments, one of which is designed specifically for identifying memory leaks. There are also command tools, including leaks, that can identify and analyse memory leaks for those who don’t use Xcode.
In my first example, the leak and its fix were so obvious that I kicked myself for not deallocating the buffer properly in the first place, then released fixed versions within hours of the leak being reported. My sparse matrices needed a little more research, although it wasn’t until after I had fixed the leak that I discovered the documentation referring to SparseCleanup().
Is there any excuse?
No. And there’s absolutely none for the second leak, which has persisted in macOS for at least two years.