How many times did you use a desktop application to end up with a frozen unresponsive window? Don’t know about you, but it happened to me more times than I can count.
There’s a core issue that causes this problem, and that is a single UI thread. There’s just one thread that can make UI changes, respond to events, and so on. If that thread is stuck in a deadlock, then the application will hang.
We’re not going to talk why there’s just one thread for UI (though that’s an interesting topic). Instead, this article is about what we are to do when our .NET application freezes. We’re going to explore tools and debugging techniques to see where the program is stuck and to find the core cause of the issue.
An application freeze is more relevant to desktop application where there’s a UI thread present that freezes user interaction. In a web application you might have hung requests on the server, which we’ll talk about as well.
1. Attach to the frozen process with Visual Studio
When you run your app from Visual Studio directly, you can hit Debug | Break All when the application freezes. Then, explore the Call Stack of the main thread to find out why the UI is stuck.
If you’re not running from Visual Studio, but VS is installed on the machine, you can attach to the hanged process and debug it in the same matter.
To attach in Visual Studio:
- Open a Visual Studio instance (no need to open a solution or project)
- Uncheck “Enable Just My Code” in Tools | Options | Debugging (this might not be necessary on a development machine)
3. In the menu choose Debug | Attach to Process (Ctrl+Alt+P)
4. Choose your process from the list and click Attach
5. Hit Debug | Break All (Ctrl+Alt+Break) to stop the runtime and debug the problem.
If you’re on a development machine with symbol files (.pdb files) included and you have the same version of the source code as the running program, you will be able to see the source code. If not, you will still be able to see the Call Stack, Threads, and Locals windows. In most cases, that’s enough to find the deadlock.
In a desktop application, the most important thing to investigate is the UI thread. If your application is stuck, that means the UI thread is stuck.
Here’s a piece of my code that causes a deadlock and freezes the app:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
And here’s what I see when attaching to the process in Visual Studio:
The image shows the main thread (UI thread) stuck on
.Wait() of the task. Looking at the other threads will reveal that a worker thread (thread-pool thread) is waiting on
Dispatcher.Invoke. So our program is hung because the worker thread is waiting on the UI thread, but the UI thread is never released because it’s waiting for the worker thread to finish.
Now that we see the problem, it’s easy to fix in a number of ways.
2. Attach to the hung process with a performance profiler
Attaching to a performance profiler is a little different than attaching with Visual Studio. With a performance profiler, instead of breaking in a specific point, you will be able to record the execution for some time and see which methods are being called during the freeze. This is very useful when the hang is caused by some kind of infinite loop, or an infinite retry mechanism.
If you’re using the excellent dotTrace profiler like me, then follow these steps:
- Open dotTrace
- Choose Attach to Running App (topmost)
- Choose your application by name or with the Drag & Drop tool
- Choose Sampling
- Click Run
- After a few seconds of recording, click “Get Snapshot and Wait”. This will open a “Performance Viewer” window
- Open the UI Thread only and investigate
Here’s me debugging a frozen application:
As you can see, the UI thread spends 100% of the recorded time in the method
PrintCustomers. dotTrace allows seeing the source code. In this case, it shows my application keeps trying to do a GET call with no success and endless retries.
public static void PrintCustomers()
HttpClient httpClient = new HttpClient();
bool retry = true;
retry = false;
var customers = httpClient.
// print customers
retry = true;
I really should be doing better error handling.
3. Attach to the frozen process with dnSpy
I wrote about dnSpy before, it’s actually an amazing tool. dnSpy allows you to debug any .NET application without having the symbols or the original source code. It decompiles the code, shows it and even enables setting breakpoints inside the decompiled code.
This means that you can attach to your hanged process, hit Break (Ctrl + Break) and see the line of code where the process is stuck on any machine, including a production machine.
Here’s me debugging my deadlock with dnSpy, after taking care to delete the symbols and source code:
Using dnSpy is pretty much like using Visual Studio:
- Download and open dnSpy
Make sure you open the same bitness executable (x86, x64) as the debugged application.
- Click Debug | Attach to process (Ctrl+Alt+P)
- Choose the stuck process from a list
- Hit Break (Ctrl+Break)
- Go to Debug | Threads window (Ctrl+Alt+h) and click on the main thread
- Go to Debug | Call Stack window and navigate to the topmost method in your own assembly. dnSpy will decompile everything, including the .NET framework, WPF, and whatever else you got going on.
- The decompiled source code will appear. You can set breakpoints, view locals, hover over variables, and so on.
4. Save a Dump to debug on another machine
For a production machine, where you can’t attach to process, you can save a memory Dump (.dmp file) to investigate later on a development machine.
A memory dump represents the entire memory at the time of capturing the Dump. When opening it in Visual Studio/WinDbg/another tool, you can see your program state at the time of saving the dump. This includes the exact line of execution in each thread, local variables and the entire memory heap if you captured a full-memory dump.
There are 3 parts to start debugging Dumps:
- Capturing the Dump file
- Opening the Dump in your tool of choice (Visual Studio, WinDbg, other tools)
- Matching to correct symbols and source code.
There are several ways to save a Dump file (.dmp). Some popular methods are:
I wrote an extensive article on saving a Dump, using various tools to debug it and matching to symbols and source code: How to Create, Use, and Debug .NET application Crash Dumps in 2019.
If you want to get a Dump from an Azure App Service, you can use Kudu tools and ProcDump. Follow this excellent tutorial.
5. Attach to a process with WinDbg
If you’ve been working on Windows applications long enough, you know that WinDbg used to be the go-to tool for windows debugging. For .NET, those days are (thankfully) gone. We now have Visual Studio and various other profilers as our first debugging choice.
Having said that, WinDbg can still save your hide once in a while. You can install it in a production environment, attach to the frozen process, and do some debugging. There are 4 reasons why you would use WinDbg:
- The production machine has nothing but WinDbg installed, and you can’t move a Dump file to a development machine for some reason.
- You need some WinDbg feature that other tools don’t have.
- You can debug the production machine with command line only. In that case, you probably need the command line alternative CDB.
- You are a WinDbg expert that can find problems with the command line faster than us youngsters with our fancy GUI tools.
To debug a frozen app with WinDbg, do the following:
- Open WinDbg
- In menu, choose File | Attach to a Process
- In menu, choose Debug | Break (Ctrl+Break)
Now you can use all the power of WinDbg to explore the memory and find the problem. You will probably want to use the SOS extension for managed code and SOSEX extension for some extra features (like automatically find deadlocks). Check out this getting-started tutorial on debugging .NET applications with WinDbg.
6. Use PerfMon counters to get clues on the core reason of the hang
You can use PerfMon to check the frozen process for clues. PerfMon is a monitoring tool (pre-installed on Windows) that allows adding counters to measure a bunch of useful metrics. It’s a bit hard at first use, but here’s a good tutorial.
Specifically, we are interested in CPU usage and memory usage of your process. If the CPU is at 0% during the freeze then it’s either a deadlock or an IO operation stuck (waiting for network request for example). If the CPU is at 100%, then it’s an infinite loop or a stuck calculation of some kind. Here’s an example of an empty infinite loop I added on the UI thread:
The counter for CPU is Process | % Processor Time.
We’re also interested in memory usage because in some rare cases, high memory consumption can cause a freeze. To see memory usage, use the counter Process | Private Bytes. If there’s a problem in memory, then you should use a memory profiler like dotMemory to investigate. A memory profiler can attach to a process or open a Dump file.
7. Automatically Save a Dump when a process freezes
This technique doesn’t help debug a frozen application, but it will show you how to automatically create a Dump file when the freeze happens. This can be useful in a number of scenarios:
- Your app freezes for a limited time and you need to catch the moment to save the Dump
- For a production server that restarts after some down time
- When the application state changes after several seconds into the freeze, and you need to save the dump file immediately
- If you want some kind of automatic reporting when your program hangs
For example, I can execute this command:
procdump -ma -h WpfFrozenApp
This will create a full memory dump (
-ma) if a process has a hung window over 5 seconds(
-h) for a process with the executable named
WpfFrozenApp. The process needs to be already running at the time of executing this command line.
Another resource that can help is my blog post series on deadlocks, which can help find the reason your app will froze in the first place.
I hope this article was helpful to you and I wish for quick unfreezes in your future. If you enjoyed this post, subscribe here to the newsletter for updates on more useful articles.