Debugging and diagnostics

In this section, you will learn how to debug problems at design time, trace problems at runtime, and use types such as Debug, Trace, Process, and Stopwatch that are in the System.Diagnostics namespace.

Debugging an application

In Visual Studio, press Ctrl + Shift + N or navigate to File | New | Project….

In the New Project dialog, from the Installed Templates list, select Visual C#. In the list at the center, select Console Application, type the name Ch05_Debugging, change the location to C:\Code, type the solution name Chapter05, and then click on OK.

Modify the template code to look like this:

using static System.Console;

namespace Ch05_Debugging
{
    class Program
    {
 static double Add(double a, double b)
 {
 return a * b; // deliberate bug!
 }
        static void Main(string[] args)
        {
 double a = 4.5; // or use var
 double b = 2.5;
 double answer = Add(a, b);
 WriteLine($"{a} + {b} = {answer}");
 ReadLine(); // wait for user to press ENTER
        }
    }
}

Press Ctrl + F5 and take a look at the output:

4.5 + 2.5 = 11.25

There is a bug! 4.5 added to 2.5 should be 7 and not 11.25. We will use the debugging tools in Visual Studio 2015 to squash the bug.

Setting a breakpoint

Breakpoints allow us to mark a line of code that we want to pause at to find bugs. Click on the open curly bracket at the beginning of the Main method and go to the Debug | Toggle Breakpoint menu, or press F9.

A red highlight will appear with a red circle in the grey margin bar on the left-hand side, to indicate that a breakpoint has been set. Breakpoints can be toggled off with the same command. You can also click in the margin to toggle the breakpoint on and off, as shown in the following screenshot:

Go to Debug | Start Debugging, or press the Start toolbar button, or press F5. Visual Studio starts and then pauses when it hits the breakpoint. This is known as break mode. The line that will be executed next is highlighted in yellow, and a yellow arrow points at the line from the grey margin bar, as shown in the following screenshot:

Tip

You can drag the yellow arrow and its highlight. When you continue executing, it will run from the new position. This is useful for moving back a few statements to rerun them or to skip over some statements.

The debugging toolbar

Visual Studio enables some extra toolbar buttons to make it easy to access debugging features. Here are a few of those:

  • Continue / F5 (green triangle): This button will run the code at full speed from the current position
  • Stop Debugging / Shift + F5 (red square): This button will stop the program
  • Restart / Ctrl + Shift + F5 (circular black arrow): This button will stop and then immediately restart the program
  • Step into / F11, Step over / F10, and Step out / Shift + F11 (blue arrows over dots): These buttons will step through the code in various ways

The following screenshot illustrates Visual Studio's extra toolbar buttons:

Debugging windows

Visual Studio makes some extra windows visible so that you can monitor useful information such as variables while you step through your code. If you cannot find one of these windows, then on the Debug menu, choose Windows, and then select the window you want to view.

Tip

Most of the debug windows are only available when you are in the Break mode.

The Locals window shows the name, value, and type for any local variables. Keep an eye on this window while you step through your code:

In Chapter 1, Hello, C#! Welcome, .NET Core!, I introduced you to the C# Interactive window. The similar, but more basic, Immediate Window also allows live interaction with your code.

For example, you can ask a question such as, "What is 1+2?" by typing ?1+2 and pressing Enter. You can also use the question mark to find out the current value of a variable:

You can execute statements of code:

As long as you have Visual Studio 2015 with Update 1, the C# Interactive window is better.

Stepping through code

From the Debug menu, choose Debug | Step Into, or click on the Step Into button in the toolbar, or press F11. The yellow highlight steps forward one line, as shown in the following screenshot:

Choose Debug | Step Over or press F10. The yellow highlight steps forward one line. At the moment, there is no difference between using Step Into or Step Over.

Press F10 again so that the yellow highlight is on the line that calls the Add method:

The difference between Step Into or Step Over can be seen when you are about to execute a method call. If you press Step Into, the debugger steps into the method so that you can step through every line in that method. If you press Step Over, the whole method is executed in one go (it does not skip over the method!).

Use Step Into to step inside the method. Hover your mouse over the multiply (*) operator. A tooltip will appear showing that this operator is multiplying a by b to give the result 11.25. We can see that this is the bug. You can pin the tooltip by clicking on the pin icon as I have done here:

Fix the bug by changing the * to +.

We don't need to step through all the lines in the Add method, so choose Step Out or press Shift + F11. Press F11 or choose Step Into to assign the return value of the Add method to the variable answer.

The Locals window highlights the most recent change in red text. The answer is correct, so choose Continue or press F5:

Customizing breakpoints

You can also right-click on a breakpoint and choose additional options, such as Conditions, as shown in the following screenshot:

The conditions for a breakpoint include an expression that must be true and a hit count to reach for the breakpoint to apply.

In the example, as you can see in the following screenshot, I have set a condition to only apply the breakpoint if both the answer variable is greater than 9 and we have hit the breakpoint three times:

You have now fixed a bug using some of Visual Studio's debugging features.

Monitoring performance and resource usage

To write the best applications, we need to be able to monitor the speed and efficiency of our code.

Evaluating the efficiency of types

What is the best type to use for a particular scenario? To answer this question, we need to carefully consider what we mean by best. We should consider the following four factors:

  • Functionality: This can be decided by checking whether the type provides the features you need
  • Memory size: This can be decided by the number of bytes of memory the type takes up
  • Performance: This can be decided by how fast the type is
  • Future needs: This depends on the changes in requirements and maintainability

There will be scenarios, such as storing numbers, where multiple types have the same functionality, so we would need to consider the memory and performance in order to make a choice.

If we need to store millions of numbers, then the best type to use would be the one that requires the least number of bytes of memory. If we only need to store a few numbers but we need to perform lots of calculations on them, then the best type to use would be the one that runs fastest on a particular CPU.

You have seen the use of the sizeof() operator to show the number of bytes a single instance of a type uses in memory. When we are storing lots of values in more complex data structures, such as arrays and lists, then we need a better way of measuring memory usage.

You can read lots of advice online and in books, but the only way to know for sure what the best type would be for your code is to compare the types yourself. In the next section, you will learn how to write the code to monitor the actual memory requirements and the actual performance when using different types.

Although today a short variable might be the best choice, it might be a better choice to use an int variable, even though it takes twice as much space in memory, because we might need a wider range of values to be stored in the future.

There is another metric we should consider: maintenance. This is a measure of how much effort another programmer would have to put in, to understand and modify your code. If you use a nonobvious type choice, it might confuse the programmer who comes along later and needs to fix a bug or add a feature. There are analyzing tools that will generate a report that shows how easily maintainable your code is.

Monitoring performance and memory use

The System.Diagnostics namespace has lots of useful types for monitoring your code. The first one we will look at is the Stopwatch type.

Add a new console application project named Ch05_Monitoring. Set the solution's start up project to be the current selection.

Modify the template code to look like this:

using System;
using System.Diagnostics;
using System.Linq;
using static System.Console;
using static System.Diagnostics.Process;

namespace Ch05_Monitoring
{
    class Recorder
    {
        static Stopwatch timer = new Stopwatch();
        static long bytesPhysicalBefore = 0;
        static long bytesVirtualBefore = 0;
        public static void Start()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            bytesPhysicalBefore = GetCurrentProcess().WorkingSet64;
            bytesVirtualBefore = GetCurrentProcess().VirtualMemorySize64;
            timer.Restart();
        }
        public static void Stop()
        {
            timer.Stop();
            long bytesPhysicalAfter = GetCurrentProcess().WorkingSet64;
            vlong bytesVirtualAfter = GetCurrentProcess().VirtualMemorySize64;
            WriteLine("Stopped recording.");
            WriteLine($"{bytesPhysicalAfter - bytesPhysicalBefore:N0} physical bytes used.");
            WriteLine($"{bytesVirtualAfter - bytesVirtualBefore:N0} virtual bytes used.");
            WriteLine($"{timer.Elapsed} time span ellapsed.");
            WriteLine($"{timer.ElapsedMilliseconds:N0} total milliseconds ellapsed.");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Write("Press ENTER to start the timer: ");
            ReadLine();
            Recorder.Start();
            int[] largeArrayOfInts = Enumerable.Range(1, 10000).ToArray();
            Write("Press ENTER to stop the timer: ");
            ReadLine();
            Recorder.Stop();
            ReadLine();
        }
    }
}
Tip

The Start method of the Recorder class uses the garbage collector (GC) type to ensure that all the currently allocated memory is collected before recording the amount of used memory. This is an advanced technique that you should almost never use in production code.

You have created a class named Recorder with two methods to start and stop recording the time and memory used by any code you run. The Main method starts recording when the user presses Enter, creates an array of ten thousand int variables, and then stops recording when the user presses Enter again.

The Stopwatch type has some useful members, as shown in the following table:

The Process type has some useful members:

Press Ctrl + F5 to start the application without the debugger attached. The application will start recording the time and memory used when you press Enter, and then stop recording when you press Enter again:

Press ENTER to start the timer:
Press ENTER to stop the timer:
Stopped recording.
942,080 physical bytes used.
0 virtual bytes used.
00:00:03.1166037 time span ellapsed.
3,116 total milliseconds ellapsed.
Measuring the efficiency of processing strings

Now that you've seen how the Stopwatch and Process types can be used to monitor your code, we will use them to evaluate the best way to process string variables.

Add a new console application project named Ch05_BuildingStrings. Add the following using statements:

using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
using static System.Console;
using static System.Diagnostics.Process;

Copy and paste the class definition for the Recorder class from the earlier project.

Tip

Best Practice

Although copy and paste is a valid technique for code reuse in some scenarios, it would be better to create a class library assembly for the Recorder class so that we can share it between multiple projects without maintaining multiple copies. You will learn how to do this in Chapter 6, Building Your Own Types with Object-Oriented Programming.

Add the following code to the Main method. It creates an array of ten thousand int variables and then concatenates them with commas for separators using a string and a StringBuilder:

int[] numbers = Enumerable.Range(1, 10000).ToArray();
Recorder.Start();
WriteLine("Using string");
string s = "";
for (int i = 0; i < numbers.Length; i++)
{
    s += numbers[i] + ", ";
}
Recorder.Stop();
Recorder.Start();
WriteLine("Using StringBuilder");
StringBuilder builder = new StringBuilder();
for (int i = 0; i < numbers.Length; i++)
{
    builder.Append(numbers[i]);
    builder.Append(", ");
}
Recorder.Stop();
ReadLine();

Press Ctrl + F5 to see the output:

Using string
Stopped recording.
7,540,736 physical bytes used.
69,632 virtual bytes used.
00:00:00.0871730 time span ellapsed.
87 total milliseconds ellapsed.

Using StringBuilder
Stopped recording.
8,192 physical bytes used.
0 virtual bytes used.
00:00:00.0015680 time span ellapsed.
1 total milliseconds ellapsed.

We can summarize the results as follows:

  • The string class used about 7.5 MB of memory and took 87 milliseconds
  • The StringBuilder class used 8 KB of memory and took 1.5 milliseconds

In this scenario, StringBuilder is about one hundred times faster and about one thousand times more memory efficient when concatenating text!

Tip

Best Practice

Avoid using the String.Concat method or the + operator with string variables. Instead, use StringBuilder or the C# 6 $ string interpolation to concatenate variables together, especially inside loops.

Monitoring with Debug and Trace

You have seen the use of the Console type and its WriteLine method to provide output to the console window. We also have a pair of types named Debug and Trace that have more flexibility in where they write out to.

The Debug and Trace classes can write to any trace listener. A trace listener is a type that can be configured to write output anywhere you like when the Trace.WriteLine method is called. There are several trace listeners provided by .NET, and you can even make your own by inheriting from the TraceListener type.

Writing to the default trace listener

One, the DefaultTraceListener, is configured automatically and writes to Visual Studio's output window; you can configure others manually using code or a configuration file.

Add a new console application project named Ch05_Tracing. Modify the template code to look like this:

using System.Diagnostics;
using static System.Console;
namespace Ch05_Tracing
{
    class Program
    {
        static void Main(string[] args)
        {
            Debug.WriteLine("Debug says Hello C#!");
            Trace.WriteLine("Trace says Hello C#!");
            WriteLine("Press ENTER to close.");
            ReadLine();
        }
    }
}

Press F5 to start Visual Studio with the debugger attached. In Visual Studio's output window, you will see the two messages. If you cannot see the output window, press Ctrl + W, O or navigate to View | Output menu.

Ensure that you show output from Debug, as shown in the following screenshot:

Configuring trace listeners

Now, we will configure some trace listeners that will also write to a text file and to the Windows application event log.

In Visual Studio's Solution Explorer, double-click on the file named App.config and modify it to look like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <system.diagnostics>
 <sharedListeners>
 <add name="file" type="System.Diagnostics.TextWriterTraceListener" initializeData="C:\Code\Trace.txt" />
 <add name="appeventlog" type="System.Diagnostics.EventLogTraceListener" initializeData="Application" />
 </sharedListeners>
 <trace autoflush="true">
 <listeners>
 <add name="file" />
 <add name="appeventlog" />
 </listeners>
 </trace>
 </system.diagnostics>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
  </startup>
</configuration>

You have configured two shared listeners—one that writes to a text file and another that writes to the application event log.

Press F5 to start Visual Studio with the debugger attached. In the console application, press Enter to close it. This will release the file that it is writing to. Click on the Windows Start menu, type event, and then click on Event Viewer:

In the Event Viewer window, expand Windows Logs, choose Application, choose the most recent log entry, and then click on the Details tab. You should see that the Friendly View option of the EventData is the message we output:

Run File Explorer, look in the C:\Code folder, and open the file named Trace.txt. If you open it with Notepad, it will look like this:

Configuring compiler symbols for .NET Framework

You might be wondering what the difference between Debug and Trace is. When you compile and run any application, it can be configured with the debug or trace compiler symbols on or off. By default, both are enabled. You can see this by double-clicking on Properties in the Solution Explorer window, and then clicking on the Debug tab.

You can see that both the debug and trace symbols are enabled. You can define your own symbols by entering them in the Conditional compilation symbols box, as shown in the following screenshot, where I have defined two symbols named KERMIT and FOZZIE:

Defining compiler symbols for .NET Core

If you chose to create a Console Application (Package) project to target the .NET Core, then you must define compiler symbols using the project.json file.

In the project.json file, add the configurations section, as shown in the following code, that specifies options for the two possible solution configurations—Debug and Release:

{
  "version": "1.0.0-*",
  "description": "Ch05_Tracing Console Application",
  "authors": [ "markjprice" ],
  "tags": [ "" ],
  "projectUrl": "",
  "licenseUrl": "",

 "configurations": {
 "Debug": {
 "compilationOptions": {
 "define": [ "DEBUG", "TRACE", "KERMIT", "FOZZIE" ]
 }
 },
 "Release": {
 "compilationOptions": {
 "define": [ "RELEASE", "TRACE" ],
 "optimize": true
 }
 }
 },
Checking compiler symbols

Modify the content of the Main method to look like this. We are using conditional compilation #if statements to only write to the trace listeners if the KERMIT and FOZZIE symbols have been defined. Note that they are Booleans so we can use operators like AND (&&) on them:

namespace Ch05_Tracing
{
    class Program
    {
        static void Main(string[] args)
        {
            Debug.WriteLine("Debug says Hello C#!");
            Trace.WriteLine("Trace says Hello C#!");

#if KERMIT
 Trace.WriteLine("KERMIT is on!");
#endif
#if KERMIT && FOZZIE
 Trace.WriteLine("KERMIT and FOZZIE are on!");
#endif

Press F5 to start Visual Studio with the debugger attached.

In Visual Studio's output window, you will see all the messages:

Debug says Hello C#!
Trace says Hello C#!
KERMIT is on!
KERMIT and FOZZIE are on!

In Visual Studio's toolbar, go to the drop-down menu that shows the list of configurations and choose Release. In this configuration, only the TRACE directive is set:

Press F5 to start Visual Studio with the debugger attached. If you see a warning message, choose Continue Debugging. In Visual Studio's Output window, you will see only the Trace message:

The idea is that you can safely put as many Debug.WriteLine statements throughout your code, knowing that when you finally compile and deploy the release version of your application they will all be automatically removed.

Tip

Best Practice

Use Debug.WriteLine statements liberally throughout your code, knowing that they will be stripped out automatically when you compile the release version of your application.

If you need more flexibility, then you can also define your own symbols, but for these, you must manually check that your own symbol has been defined using #if statements.

But what about the Trace.WriteLine statements? They are left in your release code, so they should be used more sparingly. Even these can be configured using trace switches.

Switching trace levels

In the App.config file, add the following section inside <system.diagnostics>. It can go before or after the <trace> and <sharedListeners> sections:

<switches>
  <add name="PacktSwitch" value="3"/>
</switches>

The value of a switch can be set using a number or a word. For example, the number 3 can be replaced with the word Info, as shown in the following table:

In the Main method, add the following statements before prompting the user to press Enter:

var ts = new TraceSwitch("PacktSwitch", "");
Trace.WriteLineIf(ts.TraceError, "TraceError");
Trace.WriteLineIf(ts.TraceWarning, "TraceWarning");
Trace.WriteLineIf(ts.TraceInfo, "TraceInfo");
Trace.WriteLineIf(ts.TraceVerbose, "TraceVerbose");
Trace.Close(); // release any file or database listeners
WriteLine("Press ENTER to close.");
ReadLine();

This code will check the value of the switch named PacktSwitch and only output if the level has been set.

Tip

Best Practice

Call the Close method of the Trace type to release any locks that might be held after writing to a text file trace listener. This is necessary only if you are writing to listeners that are buffered or apply locking, such as files and databases. However, it doesn't hurt to do this every time.

Press F5 to start Visual Studio with the debugger attached. If you see a warning message, choose Continue Debugging. In Visual Studio's Output window, you will see only the Trace messages up to level 3 (Info):