App hangs when optimization is turned on

Hi there,

I'm dealing with a bug in my mobile app where it hangs exclusively on devices using TestFlight or App Store builds. It doesn't occur on simulators or when launched from Xcode, regardless of the build setup. The catch is that this only happens when I'm using Xcode 15 with optimization turned on; there are no issues with Xcode 14 or Xcode 15 with -Onone.

Unfortunately, there is no crash trace available. It seems like a main thread deadlock, but I'm not using suspect functions like DispatchSemaphore. Any tips on debugging this?

You could try running the app normally, outside of the debugger. Wait until it hangs then attach the debugger to it. See "Attach to Process" in the Debug menu in Xcode.

1 Like

But you can't attach the debugger to a TestFlight build, can you? It says "Not allowed to attach to process"

  1. I'd check -Onone TestFlight build - does it crash?
  2. Then I'd check Xcode -O local build (running outside of Xcode) does it crash?

It's easier to test with (2) to avoid the TestFlight cycle, but even if you only have (1) to go with this is still very good. Start with the older commit that doesn't crash the app and find a particular commit that makes it crash (you can do this in a binary search manner or linear depending upon the number of commits in question). Once you found the right commit that started crashing the app look carefully and/or try to split the commit further if the source of the bug is still not obvious.

Not sure on this one. You may try launching the app with lldb in terminal.

  1. -Onone TestFlight build does not hang; it only hangs when it's built with -O or -Osize.
  2. A local build never hangs. I can only repro with a TestFlight build.

I haven't figured out the commit that started hanging because it's not as simple. I need to try a year-old (or even older) commit with Xcode 15 and make it a TestFlight. And those old commits have been working without any issues with Xcode 14.

BTW, I'm calling it a hang because it doesn't really crash the app until the system decides to kill it after a minute or so. It literally hangs forever until it's killed.

I see, could take some time indeed. Say, 1000 commits to try, log2(1000) ~ 10, spend 20 min on each try – 200 min – 3–4 hours...


Here's another idea: take the latest build (which has the issue). Let's say it hangs for a minute and then getting killed. Insert a code into it like so:

func appDidFinishLaunching() {
    Thread {
        sleep(30)
        // do something to crash, e.g.:
        var x = 0
        print(1/x)
        // or something similar
    }
    ....
}

and make a TestFlight build with that.

The idea is to get the crash log with all the stacks – hopefully that'll give you an insight on what other threads are doing.

Yeah, I was thinking of trying snapshotting all the stacks to gain some insight, but I wasn't sure of the best way to capture all the stacks. Okay, I should try implementing a main thread watchdog to detect a hang and then crash the app before the system kills it. Thanks for the good idea!

But you can't attach the debugger to a TestFlight build, can you?

True. But you can create a development-signed build from your Xcode archive and that will let you attach. This as a good way to separate code signing from build issues. See Isolating Code Signing Problems from Build Problems on DevForums.

Share and Enjoy

Quinn β€œThe Eskimo!” @ DTS @ Apple

3 Likes

Try to check low level synchronization primitives like this one:

public final class ReadWriteLock: LockProtocol {
  private var rwlock: pthread_rwlock_t

  public func writeLock() {
    pthread_rwlock_wrlock(&rwlock)
  }
}

In my case passing a pthread_rwlock_t and friends (like pthread_mutex_t) as inout parameter caused app hang. It was reproduced only when building in XCode 15 and running on iOS17 and seemed like deadlock. Transferring to UnsafeMutablePointer solved that.

public final class ReadWriteLock {
  private let rwlock: UnsafeMutablePointer<pthread_rwlock_t>
  
  public func writeLock() {
    pthread_rwlock_wrlock(rwlock)
  }
  
  public init() {
    rwlock = UnsafeMutablePointer<pthread_rwlock_t>.allocate(capacity: 1)
    rwlock.initialize(to: pthread_rwlock_t())
    pthread_rwlock_init(rwlock, nil)
  }
1 Like