Debugging a Card Game with IntelliTrace: Part II
This topic applies to Visual Studio 2010 Ultimate only.
In this scenario, you will learn how to use the advanced features of IntelliTrace to debug a card game application. You will turn on the IntelliTrace option to collect call information, and you will see how call information enables you to examine the execution history of your application.
Collecting call information with IntelliTrace can affect program performance. For this reason, the option is not enabled by default. For an example of how you can use IntelliTrace to debug a Visual Basic application with the default settings, see Debugging a Card Game with IntelliTrace: Part I.
Although the card game shown in this scenario is written in Visual Basic, the code is fairly simple. C# programmers with a basic reading knowledge of Visual Basic should have no trouble following along. If you would rather see scenarios that use C# code, see Debugging a Web Site with IntelliTrace: Part I and Debugging a Web Site with IntelliTrace: Part II.
You can read this topic and understand the techniques that are used without installing the sample application. If you want to see the complete code and perform the steps in this scenario for yourself, you can download the Visual Studio solution for this sample application from the MSDN Code Gallery.
Because the sample application is a card game, which depends on chance, the steps that you perform while you run the sample application might differ slightly from those shown in this topic.
Traditional debuggers show you the state of your application at the current moment. They provide limited information about events that occurred in the past. During debugging, you must infer events that have occurred in the past, based on the current state of your application, or restart your application and try to re-create those past events. If you view a call stack, for example, you see the call stack as it exists at the current moment. To see the call stack as it existed during a previous call, you have to set a breakpoint and restart the debugging session.
With IntelliTrace, you have access to call stacks and other information that is collected at many points in time. You can "time travel" between various events that happened in the past. This eliminates the need to restart your application and set a breakpoint for many debugging problems. At each point in time, examine the call stack, use the various debugger windows, and drill down into debugging data.
Collecting Call Information
By default, IntelliTrace collects information about your program when certain key events occur. These IntelliTrace events include exceptions, breakpoints, .NET Framework events, and other system events that are useful for debugging. Sometimes, a bug occurs within your own code and is not directly related to one of these events. In those cases, it is useful to see a history of procedure or function calls within your application, including the parameters, return values, and call sites. IntelliTrace can collect this call information, although it is not enabled by default.
To prepare for this debugging scenario, you have to adjust the IntelliTrace settings to collect both IntelliTrace events and call information.
On the Tools menu, click Options.
In the Options dialog box, open the IntelliTrace node, and then click General.
Click IntelliTrace events and call information.
Testing the Black Jack application
In this scenario, you will debug a blackjack card game. If you have downloaded the sample application, you should now start the card game application.
In Visual Studio, open CardGame\CardGame.sln.
On the Debug menu, click Start Debugging.
In the Black Jack window, click New Game.
A card table, with playing cards and chips, appears in the window.
Now, you must play a few hands of blackjack to test the application. Each time that a hand of cards is dealt, the application calculates and displays the total value of the cards in your hand. We want to verify that the application calculates the total correctly when the hand contains a face card, which is either a king, a queen, or a jack.
Click the 50 button to place a $50 bet.
Click the Deal button to deal cards.
Click the Hit button to receive another card.
Click Stand when you do not want to receive another card.
Continue to play until you receive a hand with a face card. In our case, we were dealt an ace, an eight, and a king. According to the standard rules of blackjack, this hand should have a value of 19 points, but the application says it has a value of 21 points.
We will now debug this problem.
Breaking the application
Locate the IntelliTrace window. In the default configuration, you will find this window docked with Solution Explorer.
In the IntelliTrace window, click the Break All link.
Debugging the card game with IntelliTrace
The IntelliTrace window provides two views of our application's runtime history: the IntelliTrace Events view and the Calls view. First, examine the IntelliTrace Events view. Locate the last Gesture event that occurred before the debugger break. Depending on the way the cards were dealt, this will be a click on either the Deal or the Hit button. In our case, it was the Hit button. Because this was the last event collected before the debugger break, it is at the bottom of the IntelliTrace Events list.
Click the Gesture event to expand the entry and see more information:
Notice the Related Views links. Click the Calls View link to sync to the Calls View.
In the Calls View, a call stack appears at the top of the IntelliTrace window. This call stack is synced to a moment in time during the execution of your application. As you use IntelliTrace, the moment in time that you are viewing can move forward or backward in time, under your control. When the timeframe changes, the Calls View changes, as does information in the other debugger windows.
The root call of your application is on the top of the stack. The current call is on the bottom. "Current" means current to the timeframe you are viewing, not current in real time. The orientation of the call stack in this window is opposite to the convention used in the Call Stack window, where the current frame is on the top.
Underneath the current call, you will see an indented list. Here, you can see IntelliTrace events that were collected for the current call, as well as calls that are made from the current call to other functions and methods.
In the Calls View, you can double-click any to view the point in time when that call was current. The Calls View and debugger windows update automatically.
You can single-click on any call or event that is indented underneath the current call. Single-clicking a call does not cause IntelliTrace to step into the call, but it moves the execution pointer to the call site within the current call.
Immediately underneath the Gesture: Clicked "Hit" (Button) event, you can see a call to CardGame.BlackJackForm.HitBtn_Click(….). Double-click that call.
The Calls View updates to show the new context. You are now viewing the state of your application at a different moment in time.
The source window has also synced to the new moment in time:
In the source window, you will see a new UI element, the Navigation Gutter. This gutter, which is gray, appears only when you have IntelliTrace call information enabled. The Navigation Gutter contains icons that you can use to move forward or backward in time. Stepping in IntelliTrace is similar to stepping in the regular Visual Studio debugger, but there are important differences. In regular Visual Studio debugging, also known as live debugging, stepping occurs line by line. In IntelliTrace, stepping occurs from one IntelliTrace event or call to the next. In live debugging, stepping causes code to execute in real time. In IntelliTrace, you are viewing a recording of events that have happened in the past. That means that you can move freely both forward and backward in time.
The next few steps show how you can use the Navigation Gutter to step through the runtime history of your application.
In the Navigation Gutter, click the Go to Next Call or IntelliTrace Event (F10) icon. You can also use the function key.
The next statement pointer moves forward to the site of the next call:
Notice that Go to Next Call or IntelliTrace Event (F10) takes you to the call site but does not step into the call. This is equivalent to single-clicking the call in the Calls view.
Click Go to Next Call or IntelliTrace Event (F10) again.
The next statement pointer moves forward to the next call site:
Click Go to Next Call or IntelliTrace Event (F10) one more time.
Again, the next statement pointer moves to the next call site:
At this point, you want to step inside the next call. So, click the Step In (F11) icon.
You are now inside Get CardGame.BlackJackGame.CurrentPlayer(). In the Calls view, you can see that Get CardGame.BlackJackGame.CurrentPlayer(). is on the bottom of the call stack.
Stepping into a call with Step In (F11) is equivalent to double-clicking the corresponding call in the Calls view. You can use the Navigation Gutter or the Calls View to perform the same navigation.
Step through the property getter by clicking Step In (F11) repeatedly until you return from the call.
When you return from the getter, click Step In (F11) one more time to step into CardGame.Player.HasBust.
When you are inside CardGame.Player.HasBust, click Go to Next Call or IntelliTrace Event (F10).
You have now reached a line of code that looks like this:
If _handProp.GetSumOfHand > 21 Then
GetSumOfHand is the function that calculates the value of the hand, so this might be the source of the bug. We will step into that function to examine the code.
Click Step In (F11) one more time. You are now looking inside the GetSumOfHand() function:
Look at the Locals window. Here, you can see the values that were current when the call to GetSumOfHand() was active:
Expand the GetSumOfHand node to see the return value for the function.
Notice that the return value, which represents the sum of the cards in the hand, is 21. We know that the correct value is 19. Something is causing the return value to be set incorrectly.
Looking at the code in this function, you can see calls to c.FaceVal, c.BlackJackValue, and ValueOfAces(). You examine the call sites for each of those calls, either by single-clicking the call site in the Calls View, or by using the Navigation Gutter to step from one call to the next. We will use the Navigation Gutter.
Click Go to Next Call or IntelliTrace Event (F10). Notice how the contents of the Locals window change. Continue to click Go to Next Call or IntelliTrace Event (F10) until you reach the call to ValueofAces:
val += ValueofAces(val, numAces)
At this point, it seems that the code is not calculating the value of the ace correctly. So, click Step In (F11) to enter the ValueofAces function.
Go to either the Locals window or the Autos window, and expand ValueofAces:
In the Calls view, click Function Exit: CardGameFrameWork.BlackJackHand.ValueOfAces, then examine the Locals window again.
Notice that the return value is still zero. numAces is 1. The return value is clearly incorrect. The code in ValueOfAces is not correctly setting the return value.
Looking at the code, we can see the reason why the return value is 0. The code contains an If statement that is incorrect.
To fix this bug, remove the If statement.
You can now stop debugging and rerun the application to verify that the fix is correct.