[Windbg Script] Extracting Performance Monitor counters from .NET application

Have you ever had a situation where you find yourself debugging a dump from ASP.NET when suddenly you notice you forgot to get the Performance Monitor log?


If sometimes you face this situation, I have great news for you: this script shows you some of the main .NET Performance Monitor counters. It gets the information from the ASP.NET process or dump being debugged.


This is one of the coolest scripts I have!


Take a look at these screenshots:








Source code for GET_PERFMON.TXT:


$$ =============================================================================

$$ Get Performance Monitor counters from mscorsvr.dll or mscorwks.dll.


$$ Compatibility: Win32/Win64  (This is a new version from 01/30/09)


$$ Usage: $$>< to run the program.


$$ Requirements: Public symbols.


$$ If necessary change the filename below to include your path and filename.

$$ By default it uses the WinDbg path and the default file name is GET_PERFMON.TXT


$$ Roberto Alexis Farah

$$ Blog: http://blogs.msdn.com/debuggingtoolbox/


$$ All my scripts are provided "AS IS" with no warranties, and confer no rights.

$$ =============================================================================


r @$t0 = 0;

r @$t1 = 0;




    as ${/v:ScriptName} MYSCRIPTS\\GET_PERFMON.TXT


.foreach(obj { lm1m })


    .if((0 == $sicmp("${obj}", "mscorsvr")) | (0 == $sicmp("${obj}", "mscorwks")))


        r @$t0 = ${obj}!PerfCounters::m_pPrivatePerf;



.if(0 == @$t0)


    .printf /D "<b>\nThis is not a .NET application!\n</b>";








             as ${/v:GCCounters} .block


                               r @$t1 = poi(@$t0) + @$ptrsize;


                               .printf "\n.NET GC Counters\n\n";

                               .printf "GenCollection 0           = 0n%d\n", poi(@$t1);

                               .printf "GenCollection 1           = 0n%d\n", poi(@$t1+@$ptrsize);

                               .printf "GenCollection 2           = 0n%d\n", poi(@$t1+@$ptrsize*2);

                               .printf "PromotedMemory            = 0n%d\n", poi(@$t1+@$ptrsize*3);

                               .printf "PromotedMemory 1          = 0n%d\n", poi(@$t1+@$ptrsize*4);

                               .printf "PromotedFinalizationMem 0 = 0n%d\n", poi(@$t1+@$ptrsize*5);

                               .printf "Process ID                = 0n%d\n", poi(@$t1+@$ptrsize*6);

                               .printf "GenHeapSize 0             = 0n%d\n", poi(@$t1+@$ptrsize*7);

                               .printf "GenHeapSize 1             = 0n%d\n", poi(@$t1+@$ptrsize*8);

                               .printf "GenHeapSize 2             = 0n%d\n", poi(@$t1+@$ptrsize*9);

                               .printf "TotalCommittedBytes       = 0n%d\n", poi(@$t1+@$ptrsize*0n10);

                               .printf "TotalReservedBytes        = 0n%d\n", poi(@$t1+@$ptrsize*0n11);

                               .printf "LargeObjectSize           = 0n%d\n", poi(@$t1+@$ptrsize*0n12);

                               .printf "SurviveFinalize           = 0n%d\n", poi(@$t1+@$ptrsize*0n13);

                               .printf "Handles                   = 0n%d\n", poi(@$t1+@$ptrsize*0n14);

                               .printf "Alloc                     = 0x%x\n", poi(@$t1+@$ptrsize*0n15);

                               .printf "LargeAlloc                = 0x%x\n", poi(@$t1+@$ptrsize*0n16);

                               .printf "InducedGCs                = 0n%d\n", poi(@$t1+@$ptrsize*0n17);

                               .printf "TimeInGC                  = 0n%d\n", poi(@$t1+@$ptrsize*0n18);

                               .printf "TimeInGCBase              = 0n%d\n", poi(@$t1+@$ptrsize*0n19);

                               .printf "PinnedObjects             = 0n%d\n", poi(@$t1+@$ptrsize*0n20);

                               .printf "SinkBlocks                = 0n%d\n\n", poi(@$t1+@$ptrsize*0n21);








             as ${/v:InteropCounters} .block


                                          r @$t1 = poi(@$t0) + 0x74;

                                    .printf "\n.NET Interop Counters\n\n";

                                    .printf "CCW         = 0n%d\n", poi(@$t1);

                                    .printf "Stubs       = 0n%d\n", poi(@$t1+0x4);

                                    .printf "Marshalling = 0n%d\n", poi(@$t1+0x8);

                                    .printf "TLBImports  = 0n%d\n", poi(@$t1+0xc);

                                    .printf "TLBExports  = 0n%d\n\n", poi(@$t1+0x10);






             as ${/v:LoadingCounters} .block


                                          r @$t1 = poi(@$t0) + 0x88;                                   

                                    .printf "\n.NET Loading Counters\n\n";

                                    .printf "Current ClassesLoaded = 0n%d\n", poi(@$t1);

                                    .printf "Total ClassesLoaded   = 0n%d\n", poi(@$t1+0x4);

                                    .printf "Current AppDomains    = 0n%d\n", poi(@$t1+0x8);

                                    .printf "Total AppDomains      = 0n%d\n", poi(@$t1+0xc);

                                    .printf "Current Assemblies    = 0n%d\n", poi(@$t1+0x10);

                                    .printf "Total Assemblies      = 0n%d\n", poi(@$t1+0x14);

                                    .printf "Time Loading          = 0n%d\n", poi(@$t1+0x18);

                                    .printf "AsmSearchLen          = 0n%d\n", poi(@$t1+0x20);

                                    .printf "Total LoadFailures    = 0n%d\n", poi(@$t1+0x24);

                                    .printf "LoaderHeapSize        = 0n%d\n", poi(@$t1+0x28);

                                    .printf "AppDomainsUnloaded    = 0n%d\n\n", poi(@$t1+0x2c);






             as ${/v:ExceptionCounters} .block


                                          r @$t1 = poi(@$t0) + 0xb8;                                   

                                    .printf "\n.NET Exception Counters\n\n";

                                    .printf "Total Exceptions       = 0n%d\n", poi(@$t1);

                                    .printf "Filters Executed       = 0n%d\n", poi(@$t1+0x4);

                                    .printf "Finallys Executed      = 0n%d\n", poi(@$t1+0x8);

                                    .printf "ThrowToCatchStackDepth = 0n%d\n\n", poi(@$t1+0xc);






             as ${/v:LockAndThreadCounters} .block


                                                r @$t1 = poi(@$t0) + 0xc8;                                   

                                          .printf "\n.NET Locks and Threads Counters\n\n";

                                          .printf "Total Contention          = 0n%d\n", poi(@$t1);

                                          .printf "QueueLength Current       = 0n%d\n", poi(@$t1+0x4);

                                          .printf "QueueLength Total         = 0n%d\n", poi(@$t1+0x8);

                                          .printf "CurrentThreadsLogical     = 0n%d\n", poi(@$t1+0xc);

                                          .printf "CurrentThreadsPhysical    = 0n%d\n", poi(@$t1+0x10);

                                          .printf "RecognizedThreads Current = 0n%d\n", poi(@$t1+0x14);

                                          .printf "RecognizedThreads Total   = 0n%d\n\n", poi(@$t1+0x18);






             as ${/v:JITCounters} .block


                                  r @$t1 = poi(@$t0) + 0xe4;                                   

                                .printf "\n.NET JIT Counters\n\n";

                                .printf "Methods Jitted    = 0n%d\n", poi(@$t1);

                                .printf "IL Jitted Current = 0n%d\n", poi(@$t1+0x4);

                                .printf "IL Jitted Total   = 0n%d\n", poi(@$t1+0x8);

                                .printf "JIT Failures      = 0n%d\n", poi(@$t1+0xc);

                                .printf "Time In JIT       = 0n%d\n", poi(@$t1+0x10);

                                .printf "Time In JIT Base  = 0n%d\n\n", poi(@$t1+0x14);






             as ${/v:SecurityCounters} .block


                                      r @$t1 = poi(@$t0) + 0xfc;                                    

                                    .printf "\n.NET Security Counters\n\n";

                                    .printf "Total RunTime Checks     = 0n%d\n", poi(@$t1);

                                    .printf "Time Authorize           = 0n%d\n", poi(@$t1+0x4);

                                    .printf "Link Checks              = 0n%d\n", poi(@$t1+0xc);

                                    .printf "Time RunTime Checks      = 0n%d\n", poi(@$t1+0x10);

                                    .printf "Time RunTime Checks Base = 0n%d\n", poi(@$t1+0x14);

                                    .printf "Stack Walk Depth         = 0n%d\n\n", poi(@$t1+0x18);               





    .printf /D "<link cmd=\"${GCCounters} ad ${/v:ScriptName};ad ${/v:GCCounters}; ad ${/v:InteropCounters};ad ${/v:LoadingCounters};ad ${/v:ExceptionCounters};ad ${/v:LockAndThreadCounters};ad ${/v:JITCounters};ad ${/v:SecurityCounters}; $$><${ScriptName}\"><b>.NET GC Counters</b></link>\n\n"

    .printf /D "<link cmd=\"${InteropCounters} ad ${/v:ScriptName};ad ${/v:GCCounters}; ad ${/v:InteropCounters};ad ${/v:LoadingCounters};ad ${/v:ExceptionCounters};ad ${/v:LockAndThreadCounters};ad ${/v:JITCounters};ad ${/v:SecurityCounters}; $$><${ScriptName}\"><b>.NET Interop Counters</b></link>\n\n"

    .printf /D "<link cmd=\"${LoadingCounters} ad ${/v:ScriptName}; ad ${/v:GCCounters}; ad ${/v:InteropCounters};ad ${/v:LoadingCounters};ad ${/v:ExceptionCounters};ad ${/v:LockAndThreadCounters};ad ${/v:JITCounters};ad ${/v:SecurityCounters}; $$><${ScriptName}\"><b>.NET Loading Counters</b></link>\n\n"

    .printf /D "<link cmd=\"${ExceptionCounters} ad ${/v:ScriptName}; ad ${/v:GCCounters}; ad ${/v:InteropCounters};ad ${/v:LoadingCounters};ad ${/v:ExceptionCounters};ad ${/v:LockAndThreadCounters};ad ${/v:JITCounters};ad ${/v:SecurityCounters}; $$><${ScriptName}\"><b>.NET Exception Counters</b></link>\n\n"

    .printf /D "<link cmd=\"${LockAndThreadCounters} ad ${/v:ScriptName}; ad ${/v:GCCounters}; ad ${/v:InteropCounters};ad ${/v:LoadingCounters};ad ${/v:ExceptionCounters};ad ${/v:LockAndThreadCounters};ad ${/v:JITCounters};ad ${/v:SecurityCounters}; $$><${ScriptName}\"><b>.NET LocksAndThreads Counters</b></link>\n\n"

    .printf /D "<link cmd=\"${JITCounters} ad ${/v:ScriptName}; ad ${/v:GCCounters}; ad ${/v:InteropCounters};ad ${/v:LoadingCounters};ad ${/v:ExceptionCounters};ad ${/v:LockAndThreadCounters};ad ${/v:JITCounters};ad ${/v:SecurityCounters}; $$><${ScriptName}\"><b>.NET JIT Counters</b></link>\n\n"

    .printf /D "<link cmd=\"${SecurityCounters} ad ${/v:ScriptName}; ad ${/v:GCCounters}; ad ${/v:InteropCounters};ad ${/v:LoadingCounters};ad ${/v:ExceptionCounters};ad ${/v:LockAndThreadCounters};ad ${/v:JITCounters};ad ${/v:SecurityCounters}; $$><${ScriptName}\"><b>.NET Security Counters</b></link>\n\n"





Read me.

Comments (39)
  1. First off, I love this blog… it’s a bit of a goldmine…  

    I’ve used a similar script for a long time and I’ve found it extremely useful… and i tried out this one but it just gave me 0 values so I debugged it a little and found that there were two issues with it (at least to get it working on my machine)

    1. the line

    r @$t1 = @$t0 + 0x8; //for GC counters for example where

    r @$t0 = ${obj}!PerfCounters::m_pPrivatePerf;  

    $t0 would then contain the address to the pointer to PrivatePerf but it is used as the pointer to PrivatePerf… one more level of indirection would be needed to grab the actual values, i.e.

    r@$t1 = poi(@$t0)+0x8

    and then for example GenCollection 1 = poi(@$t2+0x4)

    2. The second problem is a versioning problem I believe, I haven’t tested this out on 1.1 RTM, but a problem with these types of scripts using hardcoded offsets because type info is not available with public symbols, is that the offsets may change with hotfixes or service packs.

    In this case, in 1.1.4322.2300 (SP1 for Win2003) the offsets are off by 0x4, so GC counters are at

    r@$t1 = poi(@$t0)+0x4 instead of r@$t1 = poi(@$t0)+0x8

    other than that, power to you man, awesome blog:)

  2. Hi Tess! It’s a honor for me to have your comments here! šŸ™‚

    I’m fixing the script. Actually it was running fine on “some” .NET Framework 1.1 version and using m_pGlobalPerf , but as you mentioned, I use hardcoded offsets to avoid private symbols, so any change breaks functionality.

    I’m going to fix it right now!

    Thank you so much for finding these bugs!

  3. Now the script should work on .NET Framework 1.1 and .NET Framework 2.0


  4. Roberto, u r the man! says:

    this has to be the best script that one can on an asp.net dump. This is a great candidate for an extension like SOS.. maybe the new SOSEX.dll at http://www.stevestechspot.com/SOSEXANewDebuggingExtensionForManagedCode.aspx

  5. satish says:

    Roberto, can you write something similar to extract the asp.net perf counters, like requests executing / queued / rps etc?

  6. Hi Satish,

    I think so. To do that I just need to know where the information is located (I’m not from ASP.NET team) and to have some free time to create it. šŸ™‚

    Unfortunately I’m using my free time to work on a new tool (soon it will be posted), so I’ll need to think about it later.

    Thanks for sharing this great idea and for reading my blog šŸ˜‰

  7. When helping my customers with scenarios in which the symptom is high CPU, I very often end up with only

  8. With this blog post I try to explain how "magic" pointers and offsets work. I just copied the term "magic"

  9. .rip says:

    Hello, Roberto

    It’s very interesting script, however, compatibility problems still remain.

    All offsets in scripts depends on platform of target machine.

    As variant, may be try to use @$ptrsize:

    r @$t1 = poi(@$t0) + @$ptrsize;

    .printf "n.NET GC Countersnn";

    .printf "GenCollection 0           = 0n%dn", poi(@$t1);

    .printf "GenCollection 1           = 0n%dn", poi(@$t1+@$ptrsize);

    .printf "GenCollection 2           = 0n%dn", poi(@$t1+@$ptrsize*2);

    .printf "PromotedMemory            = 0n%dn", poi(@$t1+@$ptrsize*3);

    .printf "PromotedMemory 1          = 0n%dn", poi(@$t1+@$ptrsize*4);

    .printf "PromotedFinalizationMem 0 = 0n%dn", poi(@$t1+@$ptrsize*5);

    .printf "Process ID                = 0n%dn", poi(@$t1+@$ptrsize*6);

    .printf "GenHeapSize 0             = 0n%dn", poi(@$t1+@$ptrsize*7);

    .printf "GenHeapSize 1             = 0n%dn", poi(@$t1+@$ptrsize*8);

    .printf "GenHeapSize 2             = 0n%dn", poi(@$t1+@$ptrsize*9);

    .printf "TotalCommittedBytes       = 0n%dn", poi(@$t1+@$ptrsize*0n10);

    .printf "TotalReservedBytes        = 0n%dn", poi(@$t1+@$ptrsize*0n11);

    .printf "LargeObjectSize           = 0n%dn", poi(@$t1+@$ptrsize*0n12);

    .printf "SurviveFinalize           = 0n%dn", poi(@$t1+@$ptrsize*0n13);

    .printf "Handles                   = 0n%dn", poi(@$t1+@$ptrsize*0n14);

    .printf "Alloc                     = 0x%xn", poi(@$t1+@$ptrsize*0n15);

    .printf "LargeAlloc                = 0x%xn", poi(@$t1+@$ptrsize*0n16);

    .printf "InducedGCs                = 0n%dn", poi(@$t1+@$ptrsize*0n17);

    .printf "TimeInGC                  = 0n%dn", poi(@$t1+@$ptrsize*0n18);

    .printf "TimeInGCBase              = 0n%dn", poi(@$t1+@$ptrsize*0n19);

    .printf "PinnedObjects             = 0n%dn", poi(@$t1+@$ptrsize*0n20);

    .printf "SinkBlocks                = 0n%dnn", poi(@$t1+@$ptrsize*0n21);


  10. Hello, .rip

    Thanks for letting me know about this problem and thanks again for the proposed solution! Bright solution, since it’s very simple! I like it! šŸ™‚

    Have you tested it? If yes, let me know because I didn’t have opportunity yet.


  11. .rip says:

    Hello, Roberto

    Yes, I have tested ".NET GC Counters" succesfully

  12. Done! The script was changed. Thanks for the contribution. šŸ˜‰

  13. Kumar says:

    Just can’t stop wondering how awesome you and your work can be

    But a (dumb) question to you. how do you decipher from the dump the object layout of mscorwks!PerfCounters::m_pPrivatePerf.

    Many thanks for educating.

  14. Thanks for the kindly words! šŸ™‚

    Your question is fair. Since I have access to the private symbols, I got the layout from the private symbols. Easy. šŸ˜‰

  15. Kumar says:

    Thanks a lot for the response

    Just wanted to confirm that I would be resorting to dt mscorwks!Perfounters to decipher the layout.

    As you’re Roberto (alpha geek) (and I’m sure you would be aware of hacking into the perf. counters content), would it be possible to dump the contents using dc command??????

    Many thanks for educating me again!

  16. Hi Kumar,

    Using dc, dd, db, da, du and other variations you can see the content of a structure or class, however, except for strings that are easy to identify, it’ll require a little more work to understand the information. Tip: use db. With db it’s easier to identify wether the bytes are just numbers or strings šŸ˜‰

  17. Rene says:

    Hello Roberto,

    Sounds something easy to do,

    I want to display a single character from a memory address.

    example: I have the string:

    1002  a

    1003  b

    1004  c

    I do not have the n char at the end of this string.

    How can I display just the character ‘a’?

    example: .printf (char)poi(1002)

    Something like that?

    There is any directive or command to display a range of characters using .printf?

    example: .printf "%ma",poi(1002) l 3


  18. Editing memory is going to be the subject of my next Special Command article, but to answer it I’ll use commands that I’ll explain later.

    Using .printf and %ma or %mu you can print null terminated strings:

    eza 7751a9bd "abcd"  <– Creates a string with null terminator

    .printf "%ma", 7751a9bd  <– displays it

    However, if you have to print non NULL terminated strings, one approach (it may have others that I’m not aware) is:

    7751a9bd  64636261 e8ff6464 00009e54 e80008c2  abcddd..T…….  <– string has no null terminator

    da 7751a9bd  L3   <– da/du with L to specify size

    7751a9bd  "abc"

    With Unicode you use du.

    I hope it works for you.


  19. It has been a long time since my last post, but Iā€™m back on the blog. The article for today is about

  20. David says:

    A novice question: Is this method/script only valid for ASP.NET? Or can it be used towards any .NET application (like a GUI app, console app, Windows service, or web service)?

  21. rafarah says:

    David, you can run this script with any .NET application. It shouldn't break the script. Any problems let me know šŸ™‚



  22. daveblack says:

    Hi Roberto,

    I'd like to be able to extract many more of the Performance counters – both from some of the .NET Perf Counters as well as the "Process" counter.

    Any assistance you can provide in helping me is greatly appreaciated.  Thank you for your time and this awesome script!

  23. rafarah says:

    Hi Dave,

    The Process counter and others are not there, at least for CLR before version 4.0, and the only reason I could create this script is because I replaced what would be calls to methods visible with private symbols for the proper offsets.

  24. daveblack says:

    hi Roberto,

    Thanks for the reply!  Could you please provide the offsets so that I can add those counters to my copy of your script?

  25. daveblack says:

    hi Roberto,

    Thanks for the reply!  Could you please provide the offsets so that I can add those counters to my copy of your script?

  26. Roberto says:

    The offsets are in the source code.

    For example:

    printf "PromotedMemory 1          = 0n%dn", poi(@$t1+@$ptrsize*4);

    @$ptrsize is the number of bytes a pointer has based on the architecture (32 or 64 bits)

    You can track @$t1 to get the base address. šŸ˜‰

  27. daveblack says:

    I'm not quite sure what "source code" you're referring to.  

    1. Where would the source code/offsets be for the Windows and .NET Performance Counters?

    2. I thought you were only able to get the offsets for the counters you originally implemented because you have access to the private symbols which I obviously do not.

    You lost me here….

  28. rafarah says:

    Hi Dave,

    Ok, I guess now I understand your question. šŸ™‚

    You're right, you'd need access to private symbols. Because I have I got the offsets so I could create a script that works with public symbols. In other words, offsets won't be helpful to you.

    BTW, offsets instead of names are a good approach if you intend to create an script for your application and you want to be able to use Public Symbols or just hide private information from people that have access to your script.



  29. daveblack says:

    Is it possible for you to provide the offsets for the other .NET Performance Counters as well as the "Process" performance counter?

  30. rafarah says:

    Unfortunately I cannot do that. šŸ™ Sorry.

  31. daveblack says:

    Hi Roberto,

    Thanks for being patient with my questions.  I just really want to expand the counters & info that this awesome script provides.  Is there any way that I could add these?  SSCLI?  some other way?

    Thank you for your time : )

  32. Roberto says:

    Hi Dave,

    Sorry for my delay to answer. I don't know of other methods and I've never used SSCLI before. You may want to ask other bloggers.


  33. daveblack says:

    Hi Roberto,

    I'm running your GetPerfMon counter script and I'm getting back some odd values.  I'm wondering if the offsets are still valid for .NET 4.5 (x64) which is what my app is using.

    Thank you for your time!


  34. rafarah says:

    Hi Dave,

    You are correct. I haven't had time to update my debuggers scripts yet.

    Sorry about that.


  35. daveblack says:

    Hi Roberto,

    Is there anything I can do to help update the debugger scripts for .NET 4.5?  I desperately need them for some memory dumps I've been working on.  Thank you for your consideration šŸ™‚

  36. rafarah says:

    Hi Dave,

    It's my plan to update this script and the dig_stack.txt (which needs to work for 64bits ASAP). However, since I'm not using the debugger for a while during my work it'll be hard to find time. :-S

    On the other hand, not sure if you are familiar with the PerfView tool – blogs.msdn.com/…/tools-for-your-debugging-toolbox.aspx but this is a tool that can save your day if you need to analyze .NET memory/performance related issues. This tool came after this script was created otherwise I don't think I'd have created the script. (ok, maybe for fun :-))

    So take a look and let me know if it does what you need.


  37. daveblack says:

    Hi Roberto,

    I did take a look at PerfView and unfortunately doesn't quite fit my needs unfortunately.  It helps but doesn't tell me anything I can't already get from WinDbg.

Comments are closed.

Skip to main content