Click here to Skip to main content
15,868,141 members
Articles / Programming Languages / C#
Article

NetTrace - A simple, lightweight, fast debugging tracer

Rate me:
Please Sign up or sign in to vote.
4.50/5 (7 votes)
18 Dec 200619 min read 70.9K   1.3K   41   8
An extremely easy to use and incredibly fast tracer with lots of options.

Image 1

Introduction

NetTrace is a fast, lightweight, easy to use tracing package which allows the developer to  make decisions at runtime so far as what is printed out, how it's printed, and where it's printed to. It also provides assertion handling, trigger support, object dumping, and very fast speed tracing (on my machine I can get about 1200 WriteLines out in a second, but around 14K to 15K traces out in a second). It came about as a result of my dissatisfaction with other tracing packages and the facilities in System.Diagnostics. Generally, I have found that most other tracers were more difficult to use, and typically require changes to the configuration file in order to redirect trace printing or to decide what gets printed out and what doesn't. Also, they quite often make it somewhat difficult to eliminate them totally from the release code.

As a consequence, I usually fell back to Console.WriteLine for quick printing of debugging info. The first problem with this approach is that you have to then go back and remove all those Console.WriteLine statements. Later, if you want to see the information again, you have to put them back in, and once again remove them when you're done. This gets old fast, and I decided to look for a better way.

Background and Motivation

First and foremost, I wanted NetTrace to be super easy to use, and I wanted the user to be able to totally eliminate NetTrace from the release version without having to think too much about it. Typically, in order to use NetTrace, you reference it in your project, create some enums to act as trace tags (see below), and start tracing. No initialization, no post-processing, no concerns about versions, no concerns about saving options. All of this is handled automatically. By default, the retail version will automatically not include calls to NetTrace so that there is zero performance penalty, and the NetTrace DLL is not required for running the retail version. If you want to keep NetTrace in the retail version, simply define NETTRACE in the build and it's there.

Second, I was concerned about performance. One aspect of this is the elimination of NetTrace from the retail version mentioned above. Another aspect is background printing. When NetTrace starts up, it kicks off a background thread which does any IO. The foreground process simply queues up the traces without interruption. This is the primary reason for the more than tenfold increase in the speed mentioned in the intro over Console.WriteLine. This means that you can have lots of tracing potentially going on without worrying about performance problems.

Third, I wanted very much to allow the tracing to be an interactive process that could be altered and controlled at runtime. Traces can be turned on or off, redirected, more or less information can be printed out with the traces, tracing levels can be changed, listeners can be added and removed from traces, etc., etc.. This means that you don't have to go back and throw out all those Console.WriteLines. You can turn them off at runtime. If, during debugging, you decide that you really do need to check what values are being printed out by the IO traces, you can turn them on and see that information.

Fourth, I wanted to have any decisions I made persist between runs. All trace tag status is stored in the user.config so that you don't have to recheck all the traces you were working with each time you run. In most cases, all this is handled without requiring anything from the user.

Fifth, I wanted NetTrace to be language agnostic, and it is CLS compliant so that shouldn't be a problem.

Using NetTrace

The heart of NetTrace is the tracing dialog shown above, which allows the developer to change tracing options at runtime. After referencing NetTrace, a single call to ShowTraceTagDlg will bring up this dialog. I usually place it in a debug menu item which I simply hide in the release version. This dialog displays all the trace tags in your solution here, and allows you to turn them on or off. Trace tags are an essential concept in NetTrace. Each trace tag is simply an enum member. Any enum decorated with a [TraceTags] attribute has all its members added to the list of trace tags. Trace tags can also be defined in other projects/assemblies referenced in the current assembly, and will appear in the same dialog. Every type of tracing in NetTrace takes a trace tag as its first argument. If the trace tag is turned off in the dialog, none of the tracing associated with that tag will occur. By default, trace tags are listed under the name of their corresponding enum element in the trace dialog, but you can override this with a [TagDesc("Your description here")] attribute which will use the supplied description instead.

You can have more than one enum serve as a source of trace tags. Tags are therefore naturally grouped into blocks, one block for each enum. These blocks of trace tags can be turned on or off either in a block or individually. Each trace tag enum type also appears in the tracing dialog. For each enum type, you can specify which listeners it will send its member tags output to. NetTrace uses System.Diagnostics Listeners to print to, which means you can write a custom Listener and NetTrace will happily write to it. Writing tracers at runtime is not possible, however so NetTrace also makes use of internal listeners which are subclassed off of the standard log file listener and text writer listener, but they are serialized into strings which are stored into NetTraces persistence data so that they can be added or removed at runtime and persisted. Enums are listed by their names by default, but can be preceded by an [EnumDesc("Enum description here")] attribute to a user friendly descriptive string which will appear in the tracing dialog.

You can also set the type of information to print out on an enum by enum basis. The information can include the name of the trace tag used in the trace, a time stamp, the thread ID or the severity used in the trace.

Yes, that's right - all trace statements can include, in addition to the mandatory trace tag, an optional trace level. These trace levels are just the ones used in System.Diagnostics. Each enum can have its severity level set independently. Any traces which use a lower level than the level requested for that enum will be ignored.

In addition to simple Traces, NetTrace allows for several debugging facilities. They all use trace tags and, optionally, severity levels, and so can be fine tuned in the tracing dialog. These include pretty standard asserts which fire up a dialog when they fail, which allows the user to continue, to debug, or to disable the assertion (by disabling the underlying tag). Additionally, the user code can query the status of a tag (again, with or without an optional severity) and take whatever actions are appropriate based on the tag status. This allows you to put in tests that can be turned on or off at run time. The traces can also dump raw objects which will simply print all the fields in the object and their values. By supplying an array of field names, the user can specify that only certain fields be dumped. Finally, NetTrace allows for triggers which are essentially delegates which are called whenever the Trigger() function is called. Since the Trigger function takes a trace tag and the usual optional severity, the user can control whether these triggers are actually run in the tracing dialog.

The NetTrace API

Well, all that's good and well, but how do you actually use this thing? I think that the discussion in the previous section is enough for most people to understand the details of the trace tag dialog so I won't go into that here. However, what does a program on NetTrace look like at the code level? Of course, the project needs to reference the NetTrace assembly, and a "using NetTrace" statement should be placed at the top of any file which does tracing. The first thing you need to do in NetTrace is define your trace tags. This is done very simply as follows:

C#
[ TraceTags,
    // Optional Description of the enum to be used in the trace dialog
    EnumDesc("Tags in NetTraceTester") ]
enum t {
    // Optionally, user can give descriptions for
    // trace tags which will be used in the trace tags dialog.
    // Any tags which don't have a description
    // will use the enum name in the dialog.

    [TagDesc("Print out line number when user changes line in listbox")]
    ListBoxLineChanges,

    [TagDesc("Put up a message box whenever 
              the user clicks the test button")]
    DoTestOnButtonClick
} 

All the attributes except the TraceTags attributes are optional, so this can be as simple as a two element enum with a single attribute. Trace tag enums should be declared outside of any classes to allow NetTrace to find them. And that's it! No initialization - you've done all the preparation, you're ready to start tracing.

As should be apparent by now, the trace tags dialog is the "control center" of NetTrace, so let's look at how the user can activate it:

C#
ShowTraceDialog(parentWindow);

It takes a single parameter for the parent window, which can be null.

Simple tracing is done as follows:

C#
// The following prints out based only on the ListBoxLineChanges tag...
Tracer.Trace(t.ListBoxLineChanges, "Index changed to {0}", i); 

// The following prints out if the
// ListBoxLineChanges tag is on and the severity level
// for it's enum is set low enough:
Tracer.Trace(t.ListBoxLineChanges, TraceLevel.Info, 
            "Index changed from {0} to {1}", iOld, i);

Tracer is a static class in NetTrace and all calls are made through it. Notice that there are no ifdef's required. Any calls to functions which don't return values (which is most of them in NetTrace) are conditionally removed from the assembly unless DEBUG or NETTRACE is turned on. Perversely, these functions still require that NetTrace.dll be present for linking purposes when they're built, but they make no calls into it at runtime and it can safely be discarded from the shipping version if you don't intentionally leave it in by defining NETTRACE. The traces (and assertions also) take a formatting string, followed by a variable length list of parameters which are supplied to the formatting string in the final trace output.

As a side benefit of the design using enums as trace tags, each time you type in "t.", intellisense kicks in so you don't have to remember all your individual tag names.

Assertions are a simple variation on Traces and are made as follows:

C#
Tracer.Assert(t.AssertionTag,
    ValueThatShouldBeNonZero != 0, "Oops!  Your value is zero!"); 

// or...

Tracer.Assert(t.AssertionTag,
    ValueThatShouldBeZero == 0,
    TraceLevel.Error,
    "Oops!  Your value was not zero - it's {0}!",
    ValueThatShouldBeZero );

Another facility provided by NetTrace is the ability to query trace tags for their status. This allows control at runtime over arbitrary aspects of the program being debugged. For instance:

C#
#if DEBUG || NETTRACE
    if (Tracer.FTracing(t.DoSomeTesting))
    {
        Test();
    }

    // or...

    if (Tracer.FTracing(t.DoSomeTesting, TraceLevel.Error)
    {
        DoAnotherTest();
    }
#endif

Note the #if in this case. FTracing returns a value, and so can't be eliminated through conditional attributes so this is one of the rare instances when you have to explicitly put in #if statements. Of course, there's really no way around this in such cases.

Sometimes a better way is to use Triggers. This allows the NetTrace user to define a delegate which is called conditionally based on the usual trace tag/trace level conditions. This method requires a bit of setup, as follows:

C#
 Tracer.SetTrigger(t.TriggerTest, delegate(Enum tag, 
       object[] arobj) { MessageBox.Show(arobj[0].ToString()); }); 
...
integer theIntToBeShown = 0;
Tracer.Trigger(t.TriggerTest, theIntToBeShown);

// Or, as usual

Tracer.Trigger(t.TriggerTest, TraceLevel.Info, theIntToBeShown);

Note that Trigger takes a variable list of arguments which are passed on as the arguments in the trigger delegate's arobj array.

Finally, NetTrace allows you to dump an object. Currently, this simply prints out the fields in the object. Optionally, the user can pass in a list of variable names to be printed out. Since there are two optional parameters (well, really more since the last one is a variable length list of parameters), this means that there are four overloads for TraceObj:

C#
Tracer.TraceObj(t.ObjectTag, obj, 
    "This annotation is printed out with the object");
Tracer.TraceObj(t.ObjectTag, obj, TraceLevel.Info, 
    "Another annotation");
Tracer.TraceObj(t.ObjectTag, obj, 
    "This annotation is printed out with the object",
    "FirstMember", "SecondMember");
Tracer.TraceObj(t.ObjectTag, obj, TraceLevel.Info, 
    "Annotation printed with object",
    "FirstMember, "SecondMember");

An annotation is printed with the dumped object to distinguish different dumps. The "FirstMember" and "SecondMember" strings are names of member fields in the string, which allow the user to specify that only those fields are to be dumped.

Incidentally, as stated in the comments of the sources, the code for dumping objects is modified from Charlie Williams' article in CodeProject, DebugWriter - A simple property value dumper. Thank you Charlie for providing this code.

In order to organize the output from the trace statements to some extent, we allow the user to indent and unindent tracing output. For instance:

C#
Tracer.Trace(t.tag, "This won't be indented");
Tracer.Indent();
Tracer.Trace(t.tag, "This will be indented");
Tracer.Trace(t.tag, "So will this");
Tracer.Unindent();
Tracer.Trace(t.tag, "Unindented again");

Finally, there is a function which is required currently for console apps, and potentially some odd Windows Forms. Ideally, I would like to take care of saving the persistence data "automagically". This, however, requires that NetTrace regain control when the app is shutting down. As it turns out (and this came as quite a surprise to me), this isn't easy. Normally, I do this on the ApplicationExit event. This works well as long as there is an application object. To my surprise, console apps do not have an application object (so why are they console "apps"?). I tried various things to catch the death throes of a console app, to no avail. Also, a DirectX sample app did not generate an ApplicationExit event for some reason which I've yet to look into. Consequently, NetTrace currently is forced to provide a ShutdownAndSave() function which can be called in apps which don't generate an ApplicationExit event. Hopefully, I'll eventually get this nailed down, and ShutdownAndSave will be a thing of the past. For the moment, though, it should be called at the very end of such applications. If your data is persisted without it, then you don't need it. This should be the case for most "normal" Windows Forms apps. If you put it in unnecessarily, a MessageBox will pop up informing you of the fact.

Implementation

The big secret behind NetTrace is the static constructor. If a class has a static constructor, then that constructor is called whenever any method in the class is called. The static constructor for Tracer, therefore is called whenever any tracing calls are attempted. This constructor does all the initialization. The constructor looks at all types in the calling assembly and any assemblies referenced for any enumerations which have the TraceTag attribute. It skips over various assemblies such as the system assemblies, etc.. When it finds any such enumeration, it registers all the tags in that enumeration, retrieves any of the descriptive attributes for use in the trace tag dialog, loads in the persistence data, and sets values based on that persistence data. It also starts off the background IO thread. When all that is done, NetTrace is set up and ready to trace.

As a side note, it seems to me that static constructors are tailor made to implement the singleton pattern. Usually, this pattern is implemented as a class method, which checks to see if a singleton object has already been created, creates it if it hasn't, and returns the object. With static constructors, this checking is performed automatically. You can create the object in a static constructor, and just return it in the singleton class method without checking to see if it has already been created. This is much cleaner and nicer that the standard implementation. In general, if you want something done one time only for a class (and done only if that class is actually used), a static constructor is the way to go. Package initialization falls into this category also, as NetTrace proves. For some reason, in spite of what seems to me to be their obvious usefulness, static constructors don't seem to get much press, so tell all your friends!

The background printing is really just a simple provider/consumer situation for the threads involved. It's not terribly difficult (it only took me a couple of hours to implement), but has a dramatic impact on throughput. When I first implemented it, I put in some code in the test project to do tracing in a loop which ran for a second. I got about 800 traces out, which seemed like a low figure. I replaced the trace with a Console.Writeline, and got about 1200 of those. This was disturbing - I really expected the traces to run faster than the WriteLine due to the background thread. I started looking to see if there was some way to speed things up, and noticed some legacy code which I'd put in to allow for coloring trace statements and had forgot to remove. I removed them and reran with the traces in. Traces started spitting out, and it just wasn't stopping. Finally, I decided I must have somehow caused an infinite loop, and stopped things to take a look. I looked carefully at the code and didn't see anything. I made some cosmetic changes and tried again. Still the traces kept coming. I started scratching my head as the traces poured out and suddenly they stopped - at 17,000 traces! I tried it again and got 14,000 traces. I put back in the Console.WriteLine statements, and back down to 1200 being executed. It's not often you get to see a 15-20x increase in performance by removing one statement!

NetTrace persists its data to user.config with the best way I could find of doing an officially sanctioned save. This works fine, but can produce a number of small directories on your drive. I found the experience of dealing with System.Configuration to be frustrating. First of all, the documentation is horrible. Secondly, most of the documentation and examples I could find out there direct you to the use of ConfigurationElement, ConfigurationElementArray, ConfigurationSection, etc.. It turns out that this is the absolutely most difficult way I could possibly imagine to persist data. For a single array, you have to create an ugly property in a ConfigurationElement, which is then included in a ConfigurationArray, which is in turn included in a ConfigurationSection, which is finally referenced in a Configuration. I spent some time trying to figure out what advantages these complex classes and their associated retinue of attributes are supposed to confer, but due to the poor documentation (or, quite possibly, just the fact that they really are unnecessary), I never really could figure it out.

Fortunately, there is a much easier way (which still seems overly complicated) using the inexplicably rarely mentioned ApplicationSettingsBase. You still have to form a slightly weird property for everything that you wish to save in this class, but if it refers to another type, that type will be persisted by XML serialization, which is fairly easy to understand. I definitely would recommend this route over the ConfigurationElement route which seems to be the highly touted one in the literature. See ConfigInfo.cs for NetTrace's implementation. In the end, though, I would think that the seemingly simple process of serializing a structure and putting it in a file wouldn't be such a gargantuan task requiring the complexity that seems to be devoted to it in System.Configuration.

Caveats

Probably the biggest thing to be aware of is that there is no ASP.NET support right now. This may come in the future.

Another thing worth knowing is that NETTRACE currently persists its data to user.config. In many ways, this seems like the "right" place. I don't like the registry because it's difficult to edit and its general fragility. However, the only problem with user.config is that each time you build a new version, a new directory is created off in "Documents and Settings/username/Local Settings/Application Data/CompanyName/Appname" with the version number as the name. The "CompanyName" directory in this path comes from the project properties under the application tab. Press the "Assembly Information..." button on this page, and a dialog will come up with a spot for the company name. There is also a spot there for the version number which, by default is 1.0.*. The * indicates that each build gets an incremental new version number, so consequently produces a new directory and a new user.config. This is usually not a problem with the large hard disks that most people have on their machines today, but if you don't like the idea of a skillion tiny directories being created in this backwater of your hard disk, you can change the version number to something fixed like 1.0.0.0. If you don't do this, NetTrace will "upgrade" from the previous version each time a new one is created, so your settings will still persist.

Another alternative, and the one I prefer, is to remove the AssemblyVersion attribute from AssemblyInfo.cs and place it in your own code, controlling the version that way. Just don't change the version "backwards", or the upgrade option above won't work and you'll likely lose all your trace settings.

Finally, don't forget the ShutdownAndSave() function if your application doesn't persist the data automatically. This happens when an ApplicationExit event isn't generated. This is definitely the case for console apps, and I have seen it in some large forms apps also. I'm still trying to figure out why this happens in any Windows Forms apps. If it happens, you'll find that your debugging session won't shut down when you kill the app (although you can shut it down from the IDE just fine). This is because the background thread never was killed. If anyone has any great ideas to how to solve this problem, I'd love to hear from them.

Console apps will require you to reference System.Windows.Forms so that the dialog can operate properly.

NetTrace is a .NET 2.0 application and won't work with 1.x applications.

Ideas for improvement

Although I'm very happy with the current level of functionality in NetTrace, there's always room for improvement.

One high priority is to eliminate the need for ShutdownAndSave() by figuring out how to catch when an app is shutdown. It's surprising to me that this is a problem, but it is.

A lower priority for me, but one that would probably help a lot of people is to make NetTrace work in ASP.NET. I work mainly on Windows Forms, but I'd like to eventually address this situation.

I'd like also, to make a separate app which can communicate with .NET. Currently, the restriction to the listeners provided by System.Diagnostics is a bit stilting. In a separate process, I could allow for coloring trace statements to make them stand out. Also, I could allow an interface for "collapsing" object dumps similar to the way the IDE does in the locals or watch windows. Potentially, I could pass actual objects to the app and use the IDE visualizers to display them. Also, this would allow me to use the indent/unindent to outline the traces. If I somehow include a tags dialog in this process, that might be a way to take care of the ASP.NET situation. Not sure how this would sync with the app being debugged, but something could be figured out.

I may make an option to save your data to the registry to avoid producing the directory profusion spoken of above. The default will probably still be user.config.

History

  • First submission - 12/16/06.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
I've been doing programming for close to 30 years now. I started with C at Bell Labs and later went to work for Microsoft for many years. I currently am a partner in Suckerpunch LLC where we produce PlayStation games (www.suckerpunch.com).

I am mainly interested in AI, graphics and more mathematical stuff. Dealing with client/server architectures and business software doesn't do that much for me. I love math, computers, hiking, travel, reading, playing piano, fruit jars (yes - I said fruit jars) and photography.

Comments and Discussions

 
GeneralSeparate Trace Viewer Pin
dbireta28-May-09 7:30
dbireta28-May-09 7:30 
QuestionRegarding usage and binary distribution license. Pin
mali769-Feb-09 7:06
mali769-Feb-09 7:06 
AnswerRe: Regarding usage and binary distribution license. Pin
darrellp9-Feb-09 9:10
darrellp9-Feb-09 9:10 
Thanks for the nice comments. I hereby give permission to anybody to use it in any product, commercial or otherwise in whatever way they see fit. If I have to absolve responsibility for any damage, I suppose I also do that so it's as is. It was mainly because I wanted something like it for myself and just for the fun of writing it. Hope that's sufficiently legal.

Darrell
QuestionNetTrace in .NET 1.1? Pin
Michal Blazejczyk5-Jan-07 9:50
Michal Blazejczyk5-Jan-07 9:50 
AnswerRe: NetTrace in .NET 1.1? Pin
darrellp5-Jan-07 12:35
darrellp5-Jan-07 12:35 
Question[Message Deleted] Pin
ptmcomp28-Dec-06 5:48
ptmcomp28-Dec-06 5:48 
AnswerRe: Is this a product showcase? Pin
darrellp28-Dec-06 9:24
darrellp28-Dec-06 9:24 
AnswerRe: Is this a product showcase? Pin
Marc Leger28-Dec-06 12:50
Marc Leger28-Dec-06 12:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.