Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Calendar.current memory leak?

I've encountered a memory issue in an app and I've been able to break it down to the NSCalendar.

A simple view controller like this:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        while Calendar.current.component(.year, from: Date()) > 0
        {
            // why does the memory keep increasing?
        }
    }
}

Seems to cause a memory leak.

This example will obviosly block the UI thread but it should not cause the memory to continuously increase, or at least be released after the loop is done. Well at least from my understanding it shouldn't. Am I misunderstanding something fundamental here? Or is it a bug?

How do I get around this issue?

Update

Quote from the comments:

FYI - your issue has nothing to do with NSCalendar. Your issue is your while loop never allowing memory to be cleaned up

All of those Date instances are taking up memory too

Well but if I run a loop with just a date comparison im not running into the same issue. Is this because the optimiser steps in?

while Date() > Date(timeIntervalSince1970: 200)
{
    // no increase of memory here
}
like image 290
Mario Avatar asked Aug 27 '18 21:08

Mario


3 Answers

As others have pointed out, the problem is that Calendar.current.component(_:from:) is, behind the scenes, introducing an autorelease object, an object that is not released until the autorelease pool is drained.

Back in the early days of reference counting Objective-C code, the common way to return a newly allocated object that would be automatically released when the caller was done with it was to return an "autorelease" object. It was an object that would only be deallocated when you yielded back to the run loop, which would drain the autorelease pool. And you could control your high-water mark on large loops that repeatedly created autorelease objects by adding your own manual autorelease pools.

Swift doesn't natively create autorelease objects, so this issue is a bit of an Objective-C anachronism that we don't generally encounter in our own Swift code. But we have to be sensitive to this whenever writing code that loops and calls Cocoa APIs that might be using autorelease objects behind the scenes, such as in this case.

Before I dive into the solution, I'm going to tweak your example to something that is guaranteed to eventually exit. For example, let's write a routine that spins until the minute associated with the current time changes (e.g. when the current minute ends and the next starts). Let's assume that previousValue contains the current minute value.

The trick is that we need to put the autoreleasepool inside the loop. In the following example, we're taking advantage of the fact that autoreleasepool is generic that returns whatever is returned inside its closure:

while autoreleasepool(invoking: { Calendar.current.component(.minute, from: Date()) }) == previousValue {
    // do something
}

Note, if you find that pattern hard to read (it takes a while to get used to closures as parameters to methods), you can use a repeat-until loop, to accomplish largely the same thing:

var minute: Int!
repeat {
    minute = autoreleasepool {
        Calendar.current.component(.minute, from: Date())
    }

    // do something
} while minute == previousValue

As an aside, this process of having a loop that spins quickly like this is highly inefficient. Certainly, as you mentioned, you would never do this on the main thread (because we never want to block the main thread). But you wouldn't generally do it on any thread because spinning is so computationally intense. Sometimes you have to do it (e.g. doing some complex calculation on background thread anyway and you want it to stop at some particular time), but 9 times out of 10, it's code smell for some deeper problem in the design. Often judicious use of timers or the like can accomplish the desired effect, without the computational overhead.

It's hard to advise you about the best solution in your case, as we don't know what broader problem you're trying to solve. But simply be aware that spinning on a thread is generally inadvisable.


You ask:

Well but if I run a loop with just a date comparison im not running into the same issue. Is this because the optimiser steps in?

No, its simply because Date() simply doesn't introduce any autorelease objects to the mix like Calendar.current.component(_:from:) evidently does. (BTW, Apple's been good about slowly excising autorelease objects throughout their code base, so you're likely to discover at some future date, even this is likely to not require manual autoreleasepool.)

like image 67
Rob Avatar answered Oct 12 '22 18:10

Rob


At first glance, it looks like the problem might be the fact that your while loop never ends, which might prevent autoreleased objects from ever having a chance to be deallocated. Or something like that. To test that, I rewrote your code using a for loop that only runs for a million iterations. (The print statement just slows things down a bit, which makes for a nicer graph.) Here's the code:

for i in 1...1000000 //Date() < d
{
    Calendar.current.component(.year, from:Date())
    print("running")
}

When I run the code that way, I get a memory graph in Xcode that looks like this:

for loop memory graph

If a leak means memory that's allocated but never deallocated, then this certainly looks like a leak. My next step was to change the code so that it doesn't create new Date objects every time through the loop:

let d = Date(timeIntervalSinceNow: 30)
for i in 1...1000000 //Date() < d
{
    Calendar.current.component(.year, from:d)
    print("running")
}

That gives exactly the same graph. If there's a leak here, it's not the Date object that's being leaked. It's time for stronger medicine. I profiled the same code in Instruments using the Allocations and Leaks tools, and I get the following allocations:

memory allocations

So there are exactly a million NSDateComponents objects created, which is the same as the number of iterations through the loop. If there's a leak, that'd be the thing that's probably leaking. But the Leaks tool says that no objects are leaked:

leaks graph

That means that all those objects are accounted for, and are probably just autoreleased objects that haven't yet been disposed. As long as there's plenty of memory and nothing explicitly releases the pool, those objects will continue to exist. But the pool will eventually be released when the memory is needed for something else, so the fact that these old objects stay alive for longer than you need them really isn't a problem.

Update:

I looked at this a little more closely with Instruments, and you can see from the graph that the objects in question do eventually get released:

instruments allocations graph

This confirms what we suspected -- Calendar.current.component() creates some autoreleased objects, which will subsequently be released when the current autorelease pool is drained. So again, there's no leak here. It only looked like a leak because the loop in your code never exited and the autorelease pool never had a chance to be emptied.

Note also that the memory chart in Xcode's debug navigator is somewhat misleading: it doesn't show how much memory is actually in use, but rather how much memory is currently assigned to your process. So the fact that you don't see it decrease doesn't mean that your app is still using that much memory, but only that the app currently has that much to work with.

like image 30
Caleb Avatar answered Oct 12 '22 20:10

Caleb


There is no memory leak. This comes from the fact that you are abusing the main thread: thousands of expensive NSDateComponents objects are created per second, each taking 176 Bytes, which amounts in a few seconds to tens of megabytes. Calendar.current.component(_:from:) is the one responsible for all of these allocations which is calling _NSCopyOnWriteCalendarWrapper on Calendar.current.

I suspect/think it has to do with the way ARC works: since in every loop Calendar.current is referenced, so there are a lot of copy-on-writes invoked, and then getting the NSDateComponents from a new Date(). Couple that with ARC not releasing an object it thinks it is still needed, and you've got this.

As to your counterexample while Date() > Date(timeIntervalSince1970: 200): Date() is cheap since it is a number of seconds at the end of the day, and comparing unrelated Date instances wouldn't require making any copy-on-writes.

The solution is to use an autoreleasepool. With ARC, Apple says that developers do not need to deallocate objects manually from the memory. There are some cases, however, where creating an autoreleasepool could help reduce the peak memory footprint of the app. Autoreleasepools are helpful because instead of waiting for the system to release whatever objects you have made, you are telling the system to release those objects at the end of a closure. In an iOS app, the main function is running in an autoreleasepool. A drain operation of that pool will happen at the end of every main run loop. the problem arises when there are more objects created than drained.

So, you should use an autoreleasepool for the parts of your code that use up a lot of memory:

autoreleasepool {
    while Calendar.current.component(.year, from: Date()) > 0 {
        print("Hello world")
    }
}

This would slow down the rate at which the memory is being populated.

like image 1
ielyamani Avatar answered Oct 12 '22 18:10

ielyamani