Friday, January 5, 2018

High Sierra hangs your main thread when the screen is asleep!

Thanks to Ben Bird of bensoftware and SecuritySpy fame I was able to work around the problem in High Sierra. If any Mac users are interested in really good DVR software I can whole heartedly recommend his Security Spy app. It works great with XTension and we even have an interface that will let you get motion data out of the program and assigned to units in XTension for taking other actions based on that.

The problem is evident in Mac OS version 10.13.2 and may actually have been introduced in the .2 update as I don’t recall having this problem immediately upon my updating to High Sierra but only more recently. I could be completely wrong about that though.

It turns out it has nothing to do with App Nap at all, I was barking up the wrong tree about that completely. It’s a rather nasty bug in some of Apple's screen drawing code. If you have an app that has an interface and it causes interface elements to update once the screen goes to sleep High Sierra will eventually just hang. Something causes a thread contention with the screen drawing. This might not be a big problem for most user apps as if the screen is asleep then you’re not using them. They will wake up along with the screen as soon as you touch the computer again. For an app that needs to keep doing things while you’re not there like XTension does this is tragic.

My first attempt at a fix was not successful. You can subscribe to events to tell you when the screen is put to sleep or woken up. In the event where the screen is going to sleep I hid all my open windows and in the event waking up the screen I showed them again. This was not enough to stop the thread contention issue. Possibly even drawing the menu is enough to cause the problem. I am not able to disable all the drawing in the app as far as I know. Though there are calls for windows to defer updates. There may be a Cocoa way to disable all screen drawing and I’ll have a look at that later. The pressure is off right now because I was able to get it working with a background helper app to check for the the main thread hanging in the app.

Ben had suggested a thread that pinged or otherwise checked on the regular spinning of the apps main thread. I tried this, but since I’m working in Xojo I’m limited to cooperative threads. If you block the main thread in a Xojo app you also block any other threads. Which is important to know if you’re working with threads and using semaphores to manage access to resources between the threads and the main thread. So I wasn’t very confident that the pinging thread would be effective since I expected that thread to get hung as well. That was indeed the case, the second thread stopped as well and so was never able to wake the screen.

The solution, hopefully temporary until Apple fixes this, was to create a separate terminal app to do the waking of the screen. Since command line apps have no GUI they aren’t affected by the problem. The background app just listens for a single character to arrive via STDIN. If it doesn’t get one from the main app at least every 5 seconds it makes a call to the Power Manager to assert a User Activity. Using the MBS plugins for Xojo this is easy, but I suspect it could fairly easily be done with a declare or external method call to the right library.  To wake the screen I just did this:

Dim userActivityID as integer

Dim err As Integer = IOPMAssertionMBS.DeclareUserActivity( "waking screen for thread contention", IOPMAssertionMBS.kIOPMUserActiveLocal, userActivityID)
You’re supposed to keep that userActivityID around and re-use it for any previous calls, so don’t just reset it to 0 each time you make this call. Though I’m not sure if thats really necessary.

The terminal app is copied into XTension’s resources folder during compilation and I’ve created an interactive shell class in XTension that is very simple. It just launches the app and starts a timer that sends a “P” to the shells stdin every second. Since timers fire on the main thread they are all silenced by the thread contention issue and so it stops sending that ping. 5 seconds later the screen wakes up and all starts working again. If I set the timing to much less than 5 seconds then I seem to get a lot of wakeups for slightly longer program operations like regular database saves and such. The 5 seconds seems a good compromise between having it hang all the time and just keeping the screen awake all the time.

Which is the other solution. You could just call that user activity constantly or tell your users to not let the screen go to sleep at all. Both things also “solve” the problem. Interestingly enough making the request to stop app nap and adding the flag for stopping idle sleeping of the screen did not solve the problem. So App Nap is not keeping the screen awake in those cases, or perhaps the screen saver is starting or something I don’t know, but just doing that did not solve the problem.

No comments:

Post a Comment

.code { background:#f5f8fa; background-repeat:no-repeat; border: solid #5C7B90; border-width: 1px 1px 1px 20px; color: #000000; font: 13px 'Courier New', Courier, monospace; line-height: 16px; margin: 10px 0 10px 10px; max-height: 200px; min-height: 16px; overflow: auto; padding: 28px 10px 10px; width: 90%; } .code:hover { background-repeat:no-repeat; }