During my last internship, I was working on a WPF (Windows Presentation Foundation) application for 3D modeling. One particular problem that I encountered was handing UI updates.
Like most UI frameworks, event listeners in the WPF framework run on the main UI thread. In our application, we had a form GUI that controls the parameters for a 3D model and any changes in the form need to automatically update the 3D model preview. Unlike most WPF applications that seem to be just glorifed excel documents (at least to me personally), our event listeners that update the 3D model preview took a significant amount of time to run (approximately 300-800 ms).
Now you may be wondering why didn’t we just offload the 3D model calculations to a background thread and update the UI when it finishes? At first I asked the same question. It turns out that due to how WPF heavily couples the underlying numbers (
ViewModel) to the GUI (
View), I would need to refactor a signficant amount of code in order to safetly pass the
ViewModel data to a separate thread. In other words, this obscure detail basically rendered our existing architecture moot. Unfortunately for me, I did not have the luxury of time to refactor our 3D model calculation module.
Personally, I think freezing the UI for less than 1 second might be negligible. However, when the user triggers a stream of events such as using their mouse wheel on an
IntegerUpDownSpinner (you can quickly increase or decrease the value with your mousewheel), then the UI will lag for a long time as it handles all the backlogged events.
One thing I realized is that every event except the last one is relevant because the final 3D model is only dependant on the final values in the form. With this in mind, I thought about spawnning a separate thread that waits until there’s probably no more input before running the 3D model calculations.
First let’s look at the traditional code that lags the UI:
My first attempt to solve this problem is by spawning a separate thread that waits 500 ms before starting the 3D model calculations:
One thing to note in the above code is that
mIsWaitingForInputToStop is only being read/written on the main UI thread so there’s no race conditions to worry about. However the problem with the above code is that the second thread doesn’t know at the end of its sleep if there were any new input events! All this does is trigger
update() every 500 ms as long as there are active inputs. I suppose this is also desirable in some situations to give the user a live preview but it’s not exactly what I wanted.
To fix this, I need to use a second flag that lets me know if an event was recently triggered. If there was an event triggered after my second thread wakes up, then it needs to go back to sleep and reset that flag. Of course with 2 threads reading/writing to the same variable, I also need to add some mutexes to avoid race conditions.
Here is the source code to a working example of the snippet shown below. While I have tested the code below and have reasoned through it several times, I cannot guarantee that it will work 100% of the time. Like all concurrent programs, there may be some weird edge case that I have not encountered yet.
Disclaimer: The above code is an implementation that I wrote from scratch today based on what I remembered about the problem and not what I wrote at work a few months ago.