swiftdebuggingswiftuiinstruments

SwiftUI debugging hangs with instruments


I have a little freeze for a a bit more than 1 second in my app (mac OS application) written in Swift / SwiftUI.
I tried to understand what is causing this problem. Here is a screenshot of the instrument.

enter image description here.

The thing is, no matter what panel I pick, the displayed information are useless to me as I'm unable to make it match with my codebase. I have either minimum information like "High / Moderater" with a counter, or a long stack trace like this that doesn't make any sense.

enter image description here

Am I missing something? Is there a special option to activate to get a proper trace and be able to locate the problem in my code?
Thanks!


Solution

  • For an introduction to hangs (and for other people coming here), here is a WWDC session that talks about how to find and analyze Hangs using instruments. It covers what hangs are and has a few examples of Hangs in a SwiftUI app.

    However, based on your description above, it seems like you are already performing all the steps described in the session, which in relation to your problem are:

    Based on combination of "no SwiftUI body intervals in the track" but "lots of SwiftUI frames in the Profile view of the Main thread" during the Hang interval, you can tell the following

    What Makes Investigating SwiftUI Performance Hard

    The problem with analyzing SwiftUI code using Time Profiler is: Contrary to AppKit, SwiftUI calls into your code much less often. Instead you provide the structure of your UI and then SwiftUI executes code based on that structure. That means that you can easily get into a situation where SwiftUI perform a lot of work on behalf of you, based on a complicated structure you setup, but it doesn't show up in the call tree of the Profile view, because all the code that gets executed is actually in the SwiftUI framework. The code you wrote influences how SwiftUI executes, but doesn't show up in the samples itself.

    One common case for something like this happening is List and similar structures. Assume some code like this:

    import SwiftUI 
    
    struct MyList: View {
        var items: [MyItem]
    
        var body: some View {
            List(items) {
                Text(item.name)
            }
        }
    }
    

    If the name takes a long time to run, it will likely produce a call stack like this:

    main
    ...
    NSApplicationMain
    ...
    CFRunloop...
    ...
    SwiftUI...
    SwiftUI...
    SwiftUI...
    SwiftUI...
    SwiftUI...
    [... lots more frames of SwiftUI ...]
    (maybe a closure pointing to your code)
    MyItem.name.getter
    

    However, it will not show up as the body of MyList taking a long time (which is what the SwiftUI View Body instrument would show you).

    The reason is that the block passed to List is used as a closure to be executed only when the List needs to show items (basically as part of laying out the view hierarchy), which happens lazily not when MyList.body.getter is executed. MyList.body.getter is cheap, it just creates the List struct and passes a closure to it, but doesn't execute it, so it doesn't show up.

    The tricky part is that the call stack above would tell you that name takes a long time, but not what calls it, (as that's just some closure being called by SwiftUI) so it's hard to connect it to MyList.

    Even more complicated: If MyItem.name.getter does not take a long time, but returns a very long String, then the time will be spent in layout of the Text view. In that case your code will show up nowhere in the call tree of the Profile view. And because Instruments shows you no symbol names for the code that gets executed in SwiftUI you will likely not even be able to tell that it is text layout that's happening.

    How to Investigate It Anyway

    1. Look for code in the heaviest stack trace that is from your code. There may be a lot of frames of SwiftUI between the main and your code, but "your" code is often easy to find as it will be shown in bold in the heaviest stack trace (this is based on whether your have debug information for the given frame, which is usually only true for parts that you have source code for).
    2. Add some named views. E.g. replace code like the above with something like this:
    import SwiftUI 
    
    struct MyList: View {
        var items: [MyItem]
    
        var body: some View {
            List(items) {
                MyItemView(item)
            }
        }
    }
    
    struct MyItemView {
        var item: MyItem
    
        var body: some View {
            Text(item.name)
        }
    }
    

    This makes sure that when the closure for creating list items is executed, it will execute MyTestView.body.getter, source code you have access to. This should both make it show up in the SwiftUI View Body track and in the Profile view of Time Profiler.

    1. Even in the case where the name property executes quickly and layout of the Text view takes a long time this will give you some hints. You can now look at the SwiftUI detail view and how many instances of MyItemView (and other views you have) were created right before your hang and maybe this can give you an indication which of your SwiftUI views to investigate.