iosxcodeinstrumentsxcode-instruments

Why does the memory growth keep decreasing after I click "Mark Generation" in Xcode Allocations?


I'm using Allocations to profile my app to see if it has any memory issues. Every time I click "Mark Generation" I get a new memory snapshot and I can see how much memory increases during two generations. However, the growth value always decreases for a few seconds until it stops, and I'm quite confused about this. Sometimes the initial growth can be more than 20MB, but finally drops to around a few hundred KB. Why is this and which value should I trust?

EDIT: Here's my screenshots. Generation C: Initial memory growth is 78MB Generation D: Initial memory growth is 45MB. Generation C: Drop to 34MB Generation D: Drop to 198KB


Solution

  • I would suggest that you simply do not focus on “generations” list, and its “growth” column, at all. It has a lot of noise in it (e.g. a ton of stuff that is going on the background) and does not capture some salient information.

    Consider the following. I profiled an app that repeatedly allocates and deallocates a huge 70mb array. I marked generation A, then allocated the 70mb array, marked generation B, released the array, marked generation C, allocated the huge array again, marked generation D, released again, and then marked generation E:

    enter image description here

    The allocations graph accurately illustrates what I was doing in the app, but the “generations” list does not. It is telling me that generation D saw a growth of 26.95kb, even though actual memory usage went up by 70mb!

    What is generally far more useful is the “Allocations” graph. Here I will select the interval from generation C to generation D:

    enter image description here

    When switched to the allocations list, and sorted in descending order of size, I now see my large array allocation. I even see a stack trace of where the allocation happened in Foo.init. And that is indeed where the allocation is taking place:

    class Foo {
        let values = Array(0 ..< 10_000_000)
    }
    
    class ViewController: NSViewController {
        var foo: Foo?
    
        @IBAction func didTapCreate(_ sender: Any) {
            foo = Foo()
        }
    
        @IBAction func didTapRelease(_ sender: Any) {
            foo = nil
        }
    }
    

    I confess that most allocations problems are not this easy to find. I manifested an easily discovered issue by doing a single huge allocation. Because reverse engineering the allocations can be so difficult, unless I am dealing with unsafe pointers and manually allocated buffers, my first line of investigation is generally Xcode’s “debug memory graph” (see https://stackoverflow.com/a/30993476/1271826).

    I might also advise watching WWDC 2021 video Detect and diagnose memory issues and see the various other links on that page. It describes a lot of very advanced techniques.


    Regarding the changing of previous generations’ “growth” values, it safe to say that to the extent that “growth” has value at all, one can safely assume that the final value is more reliable. Real-time instruments logging (as opposed to “deferred” logging) frequently updates previous graphs and values as Instruments catches up with all of the logged events.