Using Rx to detect frozen UI

24 Jan 2013

A common problem for some applications, and a very annoying thing for users, is when the UI thread is off doing some work that takes longer than expected – and it leaves the user unable to do anything because the application is frozen.

I wanted to detect when that happens in a desktop WPF app, which feels like a good fit for an Rx based solution.

First of all, we need to notice when our application is unresponsive. If our UI thread is free to process messages, then everything is fine. But if it takes too long to process them, then we have a frozen UI problem.

So if we periodically ping our UI thread and get an answer quickly, then everything is ok. If it’s not ok, we want to record that the UI thread hasn’t answered yet, and then wait for an answer later to tell us the UI has thawed.

Pinging the UI thread is straight forward enough with Rx. We need to set up an interval that will regularly produce values (we don’t care what the values are). Then pass those on to the UI thread. We’ll generate the values (i.e. the ping messages) on the task pool, and then watch for them on the dispatcher. Like so:

Observable.Interval(TimeSpan.FromSeconds(0.25))
            .StartWith(0)
            .SubscribeOn(TaskPoolScheduler.Default)
            .ObserveOn(DispatcherScheduler.Current)

If our UI thread (i.e. the dispatcher) is busy, then producedOnTaskPoolObservedOnUI won’t be producing any values. If it goes for more than, say, one second without producing anything them we know our UI has become frozen. When our UI thread is no longer frozen, it’ll start producing values again.

To do that, the full code becomes:

var producedOnTaskPoolObservedOnUI = Observable.Interval(TimeSpan.FromSeconds(0.25))
    .StartWith(0)
    .SubscribeOn(TaskPoolScheduler.Default)
    .ObserveOn(DispatcherScheduler.Current)
    .Publish().RefCount();

producedOnTaskPoolObservedOnUI
    .Throttle(TimeSpan.FromSeconds(1))
    .Window(producedOnTaskPoolObservedOnUI)
    .Subscribe(window => window.Subscribe(_ =>
        {
            /* UI Is now frozen */

            window.Subscribe(
                onNext: __ => { },
                onCompleted: () => { /* UI is no longer frozen*/  });
        }));

The producedOnTaskPoolObservedOnUI sequence, in an ideal world, will produce values every 250ms which means the Throttle of one second will never output anything (because the UI isn’t blocked). However, if we ever go for more than a second without producedOnTaskPoolObservedOnUI producing a value, the Throttle will let the most recent value through.

When the Throttle does let a value through, we open an IObservable window (which is set to close the next time producedOnTaskPoolObservedOnUI produces a value). Then we subscribe to that window, so the first value indicates that the UI is frozen. After we get the first value, we subscribe again to the window – this time we only care about the sequence completing, which tells us that the window has closed (because producedOnTaskPoolObservedOnUI produced a value) and we’re no longer frozen. We can’t just always subscribe to OnComplete, because the Window() operator will produce empty windows if Throttle doesn’t let any values through.

Now that we have “UI frozen” and “UI thawed” detection, we can do whatever we want at those points. Such as logging what the user was doing when we noticed the frozen UI.