Seven seconds to render SwiftUI

I see, slightly newer / faster than mine, you should get a similar 200 fps reported via bodyCount. Just to be sure - that's for my example unmodified? Release build? The app is running in foreground? (this matters!). Anything else suspicious running on the machine?

Your code, copy-pasted into a fresh project with nothing suspicious running elsewhere.

body call rate is 6.1, this is the upper bound for fps (fps is usually limited to 60 or 120 fps).
body call rate is 6.1 (this is >= fps)

How do I do a Release Build?

very strange... my results:

this is how to switch to release:

then:

but i don't think this help as I don't see a difference between debug / release in this app on my machine.

As my project is no way ready, I've never needed a release build. So now running a release version, directly from the release folder, and getting a 6.9 boot call rate. Wow, a whole 0.8 improvement! There's obviously something seriously amiss here, but every other app is as one would expect. For example, I can click through the months in Calendar as fast as the mouse allows and the screen updates instantly, even at full 27" screen size. I don't think I'll be buying an M2 anytime soon.

in typical apps debug v release matters (e.g. 2x or even 10x), in this case it's not doing much, so no surprise.

What is the window size visually? Mine was like half a 15 inch screen wide and some 1/4 of screen height tall.

If you comment out all the buttons and leave just this, what will it show?

    return Text(model.infoString).font(.title)
        .frame(width: 500, height: 100)

Elephant in the room - besides slightly different computers (although of a similar age) we are using different Xcode versions and different macOS versions.... I'd recommend you to run your test (or my test) on a couple other computers.

With your buttons commented out, it runs at 200 body calls! Interesting?

My app on screen is about 25 x 23cm. (25cm is roughly 10")

My wife has a MacBook Air (also on latest macOS), but when I've tried to copy one of my apps across I get into certificate problems. I'll give ago tomorrow.

Some say tomorrow never comes, but here we are after only 3 days with a result for running the code on my wife's MacBook: 4.8 counts/second.
Compiled on the iMac with Release Build.

import Foundation
import SwiftUI

private var startTime = CFAbsoluteTimeGetCurrent()
private var bodyCount = 0

class Model: ObservableObject {
    @Published var infoString = " "
    @Published var changeCount = 0
    
    init() {
        Timer.scheduledTimer(withTimeInterval: 1/200, repeats: true) { [self] _ in
            change()
        }
    }
    
    func change() {
        changeCount += 1
    }
}

struct ContentView: View {
    @StateObject private var model = Model()
    
    var body: some View {
        
        bodyCount += 1
        let currentTime = CFAbsoluteTimeGetCurrent()
        if currentTime - startTime >= 2 {
            let fps = Double(bodyCount) / (currentTime - startTime)
            let s = String(format: "%.1f", fps)
            model.infoString = "body call rate is \(s) (this is >= fps)"
//            print("body call rate is \(s), this is the upper bound for fps (fps is usually limited to 60 or 120 fps).")
//            print(model.infoString)
            bodyCount = 0
            startTime = currentTime
        }
        let buttonsPerRow = 10
        let buttonsRows = 10
        
        return ZStack {
            VStack {
                Text(model.infoString).font(.title)
                    .frame(width: 500, height: 100)
                ForEach (0..<buttonsRows, id: \.self) { j in
                    HStack {
                        ForEach (0..<buttonsPerRow, id: \.self) { i in
                            let buttonTitle = "Button " + String(j * buttonsPerRow + i)
                            Button(buttonTitle) {
                                print("button \(buttonTitle) pressed")
                            }
                        }
                    }
                }
            }
            .padding()
        }
    }
}

Your code with id: \.self added to ForEach.

I'm tempted to think my Xcode is linking old versions of something and making a right mess of it. So I'm about to try uninstalling and deleting everything Xcode, and reinstalling.

I fear the situation could be worse... it could be the new version of either Xcode or macOS responsible for the slowdown... Note that my mac is far from being fast by today's standards:

MacBook Pro is mid 2015
Big Sur
Xcode 13.2

BTW, you may have several Xcode's (if you have enough disk space! Xcode is very disk hungry). (I always download Xcode from here Sign In - Apple, never download it from AppStore). Use this handy matrix to see what's compatible with your macs.

Note - when you test the speed always have the app frontmost (when it is in background for a number of seconds system stops giving it enough time unless there are the precautions in the app - and in this case there is no).

Perhaps update your dev forum post with the new information, e.g. "this tiny simple standalone app, getting 120+ fps on this 7 years old mac but only 5 fps on this new mac".

Several hours wasted reinstalling Xcode, and no appreciable increase in performance.

Screenshot 2022-06-17 at 20.31.48

I think Apple needs to get back to the drawing board on this, and, as predicted, my post on the Apple developer forum has received zero response.

Curiouser and curiouser.

Try this easy thing (fixed frame for buttons). does it change anything?

...
Button(buttonTitle) {
    print("button \(buttonTitle) pressed")
}
.frame(width: 90, height: 20)

I will ask a friend or two to run this test on different machines to gather statistics.
Looks like some major performance regression (if it is in the new version of macOS itself).

Have you thought of opening a DTS incident? (2 incidents are free per year, IIRC).

In your post to dev forums on Apple I'd be me more positive and include the information we gathered here (with the small stand alone test app and results you are getting including results from my computer).

Bedtime here, so I'll give it a try tomorrow. The thing is I began posting on the Dev Forum about slow rendering last summer - with no replies. That forum is drowned with IOS issues. Swift.org is much better.

OK, here are the stats from two pals. Unfortunately this looks like a regression and what you observe is rather a norm (and what I observe is rather an exception, perhaps because I am on Big Sur):

Mac Book Air, M1 2021
macOS Monterey (?)
Xcode 13.4
fixed frame: 15
flex frame: 11

MacBook Pro 15 inch 2017, 3.1 GHz, 16 GB RAM.
macOS Monterey 12.4
Xcode 13.2
fixed frame: 10
flex frame: 6

What do we do about it is unclear... 100 buttons is not that much.... hmm

We all go out and buy an M2. Then an M3... Something I think Microsoft has often been accused of. It's called bloatware.

I'll post a link to this thread on the Development Forum.

Practical workaround would be to expose a "combo view" written in UIKit (that view will have those buttons). And irt SwiftUI - it will have some small number of views. Not ideal but should be quick, and still "swiftUI-ish" at least partially.

Have a look. This works much faster for me (as it was fast for me already I increased the number 10 fold locally).

import Foundation
import SwiftUI

private var startTime = CFAbsoluteTimeGetCurrent()
private var bodyCount = 0

struct Cell: Identifiable, Equatable {
    let id: Int
}

struct Row: Identifiable, Equatable {
    let id: Int
    var cells: [Cell]
}

let buttonsPerRow = 10
let buttonsRows = 10

class Model: ObservableObject {
    @Published var infoString = " "
    @Published var changeCount = 0
    
    var rows: [Row]
    
    init() {
        rows = (0 ..< buttonsRows).map { j in
            let cells = (0 ..< buttonsPerRow).map { i in
                Cell(id: j * buttonsPerRow + i)
            }
            return Row(id: j, cells: cells)
        }
        Timer.scheduledTimer(withTimeInterval: 1/200, repeats: true) { [self] _ in
            change()
        }
    }
    
    func change() {
        changeCount += 1
    }
}

struct ContentView: View {
    @StateObject private var model = Model()
    
    var body: some View {
        
        bodyCount += 1
        let currentTime = CFAbsoluteTimeGetCurrent()
        if currentTime - startTime >= 2 {
            let fps = Double(bodyCount) / (currentTime - startTime)
            let s = String(format: "%.1f", fps)
            model.infoString = "body call rate is \(s) (this is >= fps)"
//            print("body call rate is \(s), this is the upper bound for fps (fps is usually limited to 60 or 120 fps).")
//            print(model.infoString)
            bodyCount = 0
            startTime = currentTime
        }
        
        return VStack {
            Text(model.infoString).font(.title)
                .frame(width: 500, height: 100)
            
            SubView(rows: model.rows)
        }
    }
}

struct SubView: View {
    let rows: [Row]
    
    var body: some View {
        VStack {
            ForEach(rows) { row in
                HStack {
                    ForEach(row.cells) { cell in
                        let buttonTitle = "Button " + String(cell.id)
                        Button(buttonTitle) {
                            print("button \(buttonTitle) pressed")
                        }
                        //.frame(width: 90, height: 20)
                    }
                }
            }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Why it is faster - I don't know exactly. I suspect that the way it is structured now makes it easier for SwiftUI diffable machinery to avoid comparing the large part of view tree (made of buttons) as it knows that "rows" field didn't actually change.

Note that if the buttons did change (as it would be in a more realistic example, where you need to change the title, etc) - that part of tree will have to be compared (and rendered if needed). Practical trick would be to rate limit the button titles (etc) changes to, say, once per second.

SwiftUI makes code short and certain things super easy. There are some gotchas of course we have to cope with when using it.

I don't know a good forum for SwiftUI questions... It is definitely not Apple dev forums :slight_smile: and Swift dot org doesn't want to have SwiftUI related topics discussed. Try stackoverflow, it's ok.

At first I was impressed, getting between 170 and 200 !

BUT, adding another Text field, and updating it every 300 milliseconds, slows everything back down to single figures. Real-time data changes.

Nevertheless, I have experimented with Identifiable, Equatable on my API structs, but they are not allowed to coexist with Codable. In my case, being able to use an Actor to buffer incoming data before handing to Publish would be useful, but here too are barriers.

// UPDATED
import Foundation
import SwiftUI

private var startTime = CFAbsoluteTimeGetCurrent()
private var bodyCount = 0

struct Cell: Codable, Identifiable, Equatable {
    let id: Int
}

struct Row: Codable, Identifiable, Equatable {
    let id: Int
    var cells: [Cell]
}

let buttonsPerRow = 10
let buttonsRows = 10

@MainActor
class Model: ObservableObject {
    @Published var infoString = " "
    @Published var changeCount = 0
    @Published var theCount = 0
    
    var rows: [Row]
    var counter = 0
    
    init() {
        rows = (0 ..< buttonsRows).map { j in
            let cells = (0 ..< buttonsPerRow).map { i in
                Cell(id: j * buttonsPerRow + i)
            }
            return Row(id: j, cells: cells)
        }
        Timer.scheduledTimer(withTimeInterval: 1/200, repeats: true) { [self] _ in
            change()
        }
        Task {
            while true {
                theCount = counter
                counter += 1
                try await Task.sleep(nanoseconds: 300_000_000)
            }
        }
    }
    
    func change() {
        changeCount += 1
    }
}

struct ContentView: View {
    @StateObject private var model = Model()
    
    var body: some View {
        
        bodyCount += 1
        let currentTime = CFAbsoluteTimeGetCurrent()
        if currentTime - startTime >= 2 {
            let fps = Double(bodyCount) / (currentTime - startTime)
            let s = String(format: "%.1f", fps)
            model.infoString = "body call rate is \(s) (this is >= fps)"
//            print("body call rate is \(s), this is the upper bound for fps (fps is usually limited to 60 or 120 fps).")
//            print(model.infoString)
            bodyCount = 0
            startTime = currentTime
        }
        return VStack {
            Text(model.infoString).font(.title)
                .frame(width: 500, height: 100)
            Text("counter: \(model.theCount) ")

            SubView(rows: model.rows)
        }
    }
}

struct SubView: View {
    let rows: [Row]
    
    var body: some View {
        VStack {
            ForEach(rows) { row in
                HStack {
                    ForEach(row.cells) { cell in
                        let buttonTitle = "Button " + String(cell.id)
                        Button(buttonTitle) {
                            print("button \(buttonTitle) pressed")
                        }
                        //.frame(width: 90, height: 20)
                    }
                }
            }
        }
        .padding()
    }
}

//struct ContentView_Previews: PreviewProvider {
//    static var previews: some View {
//        ContentView()
//    }
//}

:+1:

I must admit I don't know actors/async/await well to reasonably comment this fragment (other than saying that it looks dodgy to me). Try one thing at a time. E.g. use a 300ms timer rather than Task + await + sleep, I bet that will be fast (if not - fix the geometry of the added string to, say, .frame(width: 100, height: 30)

I'm using Codable with Identifiable/Equatable without a problem.

I'd recommend you start without actors and then once you know SwiftUI gotchas relatively well - switch to actors. Otherwise you are dealing with two new things at once, it is harder.

BTW, Codable/Identifiable/Equatable/actor topic discussions are ok on swift dot org.

First, I want to thank you for spending so much time on this.

  • Changing from Task.sleep to Timer.scheduledTimer made little differance.
  • Codable/Identifiable/Equatable works well in the example.
  • Perhaps I'll need to change each and every API value to be that way.

A lazier approach could be to have each rectangular box in my screen grab own its own state - running concurrently along side the others. In fact, I've arranged my API with this in mind - I just haven't found a way to implement views.

For now, I must get back to working on the servers and wait to see what the next SwiftUI release delivers.

PS. Still no replies on th Developer Forum.