A Day in the Life

A day in my life. Thoughts on leadership, management, startups, technology, software, concurrent development, etc... Basically the stuff I think about from 10am to 6pm.

9/28/2007

OnUserPreferenceChanged Hang

You think your code is clean and all is well in the world when all of a sudden your users are starting to report that your application is hanging. You research the issue and discover that from time to time your application hangs when it receives a WM_SETTINGCHANGE message or an OnUserPreferenceChanged event. Ivan Krivyakov did a very thorough write up of what's happening which you can find here. And Microsoft is supposed to have a knowledgebase article out soon about this.

We just ran into this problem and we have learned a few things beyond what Ivan presented. First…did you know that in .NET 2.0 when you create a Control or Form (window) object…it really doesn't exist? For performance reasons Microsoft delays the actual window creation until the window becomes visible or a handle request is made. On the surface this looks innocent enough but if you took the time to read Ivan's report you realize that the final action of window creation may NOT happen on the main UI thread. Where you probably started it.

And don't think you're going to get a CrossThreadException on this one. Even with the CheckForIllegalCrossThreadCalls flag set no exception was thrown. Nor was an exception thrown when the application encountered this problem outside the debugger. Which the documentation says should happen.

Ivan's Freezer code worked very well (he has a link on The Page for some code, you'll want that) and made it very easy for me to reproduce the problem. So step one whenever debugging something like this is to reproduce it on the developer's machine. Freezer enables just that.

Force Window Creation

One thing that Ivan's write-up didn't make clear to me was how to get around this problem. I have confirmed from the Microsoft support guy (Trevor) that the code below will work if you've identified the correct window.

You basically have to force the window creation and you can do that in one of two ways: 1) make the window visible with a Show() or 2) request the Handle. I used the code below:


List lstHandles = new List();
IntPtr hTemp;
foreach (Control myCtrl in Controls)
{
hTemp = myCtrl.Handle;
lstHandles.Add(hTemp);
}

hTemp = Handle;
lstHandles.Add(hTemp);
lstHandles.Clear();


I put the handles in a temporary buffer because I wasn't sure if the optimizing compiler would drop a simple assignment loop like this:


IntPtr hTemp;
foreach (Control myCtrl in Controls)
{
hTemp = myCtrl.Handle;
}

hTemp = Handle;


Identifying the Hanging Window

We had not correctly identified the problem Window. To do that we needed Trevor's suggestion which sent us down the correct road. Trevor suggested using Spy++ to identify what threads our windows were running on.

This was a new use case for me (with Spy++), I had already thrown Spy++ out as a tool for this problem because with Spy++ running the hang hung my entire desktop and nothing worked but the good old three finger salute, to get a Task Manager up.

The trick was to not start Spy++ until we were ready to run the test. So I got my application to the area I knew would hang (with the help of Freezer), and then started Spy++. Once in Spy++ you'll want to do the steps below to find that "bad" window:

1. Select Spy->Processes
2. In the Process dialog find your process
3. Expand your process
4. Expand the threads with the + sign
5. Look for GUI elements on those threads. If you find an element on a non-UI thread...you have found culprit.

You might think that it would be obvious and clear where all your windows are but that wasn't the case for us. Down in our audio code an engineer had created a window to pass to the SetCooperativeLevel(). This was the problem window. A window with no title…so we basically stepped through the process until we saw that Spy++ now contained a window on a non-UI thread.

What was interesting here is that the Window was actually created much earlier but only finished being created on the call to SetCooperativeLevel(). So once we discovered where the system thought the window was created we had to back up the callstack to find the actual window creation location.