Instruments: what is static AppName.$main() [inlined]

I have a performance issue with a Mac SwiftUI app. Using instruments I see hangs reported. When I zero in on a hang I see that the time profiler reports most of the hang -- in one example 658 out of 687 ms -- being in 'static AppName.$main() [inlined]'.

I think that is saying my app is busy, but busy doing what?

The "hangs" are related to SwiftUI table processing. User experience is something like this: select an item, see the view changes based upon selection show up a second or two later. The duration of the hang increases with the number of items in the table. I'm testing with 100 ~ 1200 table entries.

Accepted Reply

My issue was self inflicted. Anything that messes with the number of rows to be displayed will have drastic performance impact.

TableView {
  // ...
} rows: {
  ForEach(foos) { foo in
     TableRow(foo)
  }
}

is acceptable. I was a using conditional to determine if TableRow should be invoked for each item. Don't do that. Filter the array of input if necessary.

That was my fix. You may be running into a different issue.

Replies

Hi marchyman,

I'm guessing there are other methods being called by static AppName.$main() [inlined], but they might be framework functions and thus collapsed by default, depending on which view you look at. If you look at the heaviest backtrace view on the bottom right, you can click the small bottom at the top right of that backtrace view to toggle showing callings from functions that are not part of your app, e.g. from system frameworks. Sometimes this can give you a hint at what might be going on and what you can optimize.

If you can post a screenshot of your Instruments trace document, we can help you interpret it.

However, you mention SwiftUI, so here are some additional guesses what might help:

What SwiftUI view are you using specifically? You are talking about a "table". Are you really using the SwiftUI Table view, or maybe making something that looks like a table using a List or maybe VStack or Grid? List and Table should be lazy and only compute as many bodies as are needed for one screenful of content, so their performance shouldn't change much if you have 100 or 1200 elements (assuming less than 100 fit on screen at once). Given that you say the number of elements makes a difference you are likely either using a non-lazy view (like Grid or VStack), in which case switching to the lazy variant might be an option. Take a look at Stacks, Grids, and Outlines in SwiftUI, which talks about some of these cases. I you are already using a lazy view, it might be that the collection you pass to ForEach takes a long time to compute. E.g. for code like this:

List {
    ForEach(model.loadAllItemsToDisplay()) {
       // the contents of each list item
    }
}

If loadAllItemsToDisplay would take a long time to run you'd see an effect like this. However, if loadAllItemsToDisplay were implemented in your code, it should also show up in the heaviest backtrace then, so it's a bit unclear what's happening in your case without more information.

Another instrument that might be useful in you case is the "SwiftUI View Body" instrument. You can add it to your document by clicking on the + button in the top right, which shows the "Instruments Library" and then searching for "SwiftUI" in there. Simply drag the "View Body" instrument to your document and record again. The "View Body" instrument should show you each view body that gets executed and maybe gives you some insight into which one takes a long time.

I see I forgot to enable email notifications of replies... fixed.

I'm using a Swift table with 4 columns. In testing I replaced the columns with a fixed Text view to see if that was an issue. It wasn't. There was in issue in row: closure to the table in that the array consisted of large-ish structs and a lot of time was being taken allocating and freeing memory to copy the array entries. I changed the struct to a class and saved about 40% of the delay time. Still, there is something going on. When I select an item in the table the user experience is 1) the row turns blue, 2) longish delay, 3) text in the first column changes color. Normally lots of other stuff goes on but I disabled all of it looking for the source of the longish delay.

That delay is the "hang" reported above. If I show system libraries it looks like this:

Note that I also scrolled the tracks to show View Body and the Core Animation Commits -- fwiw I am not doing any additional animation over whatever is built into SwiftUI.

I see you already selected the time of the long CoreAnimation commit. But you still have the "Time Profiler" track selected, which shows you all CPU usage over all processes. For a hang, it's only relevant what happens on the Main Thread. The process track (the last track called GeoTag in your first screenshot), shows what's going on in your process. To the left of "GeoTag", there is a small disclosure indicator. If you click it, it will reveal one track per thread in your process. Select the one which is called "Main Thread" to limit the data in the "Profile" view at the bottom to only the activity on the main thread and exclude CPU Usage from other threads.

Does that narrow it down in any way?

Also, could you click on the "Call Tree" button to show which filter settings for the Call Tree you have applied? Also, please right-click on a cell in the heaviest stacktrace view to show the context menu for it and show how you configured it. The "heaviest stack trace" in your 2nd screenshot is surprising to me as it seems to say we have 654 samples in the GeoTag process, but then only 22 in objc_msg_send, so I'm wondering where the other >600 samples went. Similarly, in your first screenshot, I'm surprised that there is only a main node and nothing else in the heaviest stack trace view.

Thanks for your help.

The Call Tree was set to Invert Call Tree in the above screen shots. Selecting the GeoTag main thread gives close to the same output as selecting the Time Profiler. This time I've selected the main thread. The selection is different from above as I don't remember which hang I'd selected. Also, this time I've hidden system libraries.

Also, please right-click on a cell in the heaviest stacktrace view to show the context menu for it and show how you configured it.

You can probably tell that I am an Instruments novice.

Oh, with Hide System Libraries the call tree and Heaviest Stack Trace makes sense. And Invert Call Tree explains why we started with objc_msgSend. I'd recommend disabling both options again:

  • Invert Call Tree Is not as useful for finding hangs, as you often want to simply do less work instead of making a leaf-function in the code faster. Similarly, for hangs it's often more useful to look at the high-level picture of what's going on, not at the details. For both cases looking at the non-inverted call tree is more useful.
  • "Hide System Libraries" is generally a good idea to simplify the call tree and focus on your code. But your call tree shows relatively clearly, that the time is not actually spent in any of your code. ~97% of CPU Usage falls into main and then disappear in calls to system frameworks and your code doesn't seem to be directly involved anymore. So by hiding system frameworks you are now also hiding where all the work is happening, so nothing interesting is left.At this point you'll have to start digging through what the system frameworks are doing on behalf of your app and see whether you can influence that somehow.

Thus, turn off "Hide system Libraries", select the Main Thread and a hang again. Then select the first node (the process node) in the call tree and let's look at what the heaviest call stack trace view shows. Click the double-t-shaped button at the top right of the heaviest stack trace to make the heaviest stack trace also show the function calls in system frameworks. Then right-click on a frame in the heaviest stack trace view and check "Library Name" and "Frame #" from the context menu. The frame number makes it easier to see how deep your callstack is (SwiftUI call stacks can easily be more than 100 frames deep) and checking "Library Name" will show you for each frame at least from which framework it comes and give you a bit of an idea of what's going on, even if you cannot see the exact function name.

Let's see what we can find from this.

But in general, There seems to be some opportunity for Instruments to be doing a better job of telling you what's going on in SwiftUI here. Would you have time to file a Feedback with this trace file? (if you can attach an example project, even better. Otherwise maybe you can give a rough description what kind of SwiftUI construct you are using?) In the feedback report, please explain that you are encountering a hang in your app when using SwiftUI but all the work seems to be happening in SwiftUI itself, but there doesn't seem to be a good way to find out what your code is doing that causes SwiftUI to take a long time.

I've tried to create a sample project to show this issue but have not been successful. There is something about my application code that triggers whatever is happening that I'm missing when I create a sample. My app slows way down with the number of entries. At about 1200 entries it takes 2-3 seconds to fully process a change in selection . A test app with 10,000 entries shows no slowdown at all.

I will still create a feedback report. The app source is on GitHub.

  • Something is still unexpected about call tree/stack trace. We seem to go from 697 samples / 697ms to 31 samples / ms directly without any indication where all the other samples went. I currently don't have a good explanation why this would happen, given that you don't seem to have any other CallTree modifications/filters applied. Please attach one of these traces to the Feedback report. If you can share the feedback ID here once you filed it, I can find it more easily.

Add a Comment

I was preparing feedback, but using my app as it exists on github, not the version I was using above where I replaced all views not related to the slowdown with a simple Text view. With about 1200 items in the table instruments reports a severe hang of 2.21 seconds. The slowdown is in SwiftUI code. But certainly I'm doing something to trigger that slowdown.

Anyway, I'll have to re-create the test version of the app to file a feedback addressing the unexpected call tree/stack trace. Assuming I can reproduce it.

  • If it's easy enough to reproduce and we can download the source code from a GitHub repo, you can also simply add the GitHub link to the Feedback report with steps how to build and run and reproduce the hang.

Add a Comment

I submitted FB12211784 the end of May. It contains a do-nothing-much app that creates a two column table of 10K entries. Selecting an item in the table causes the color of the text in the first column to change. On my 2019 intel iMac there is about a 140 msec delay between selection and the color change. That is noticeable. I just re-ran the code using a brand new Mac Studio with M2 Max. It still takes over 100 msec. Still noticeable.

I'm having the same issue--did you manage to find a fix? There's shockingly little information about this issue online.

My issue was self inflicted. Anything that messes with the number of rows to be displayed will have drastic performance impact.

TableView {
  // ...
} rows: {
  ForEach(foos) { foo in
     TableRow(foo)
  }
}

is acceptable. I was a using conditional to determine if TableRow should be invoked for each item. Don't do that. Filter the array of input if necessary.

That was my fix. You may be running into a different issue.