Posts
69
Comments
233
Trackbacks
162
May 2009 Entries
How To Read .NET Performance Counters Correctly

Managed Performance counters are tricky (or broken, it depends how you look at them) to read when you have more than one process with the same name running managed code. Each performance counter gets as instance name a unique identifier

  1. ManagedApp
  2. ManagedApp#1
  3. ManagedApp#2
  4. ...

If you want to know for a specific process identified by its process id thing become tricky. There is a counter in the .NET Memory category called Process ID which enables us to find out the correct counter instance name without guessing.

To find the correct instance name here is a little helper class which does it in a semi performant way:

 

using System;

using System.Diagnostics;

using System.IO;

using System.Threading;

using System.Globalization;

using System.Runtime.Remoting.Messaging;

 

namespace PerformanceCounterRead

{

    public class PerfCounterReader : IDisposable

    {

        PerformanceCounter myMemoryCounter;

        const string CategoryNetClrMemory = ".NET CLR Memory";

        const string ProcessId = "Process ID";

        const int ProcessesToTry = 40;

 

        public PerfCounterReader(int processId) : this(Process.GetProcessById(processId))

        {

        }

 

        string GetInstanceNameForProcess(int instanceCount, Process p)

        {

            string instanceName = Path.GetFileNameWithoutExtension(p.MainModule.FileName);

 

            if (instanceCount > 0) // Append instance counter

            {

                instanceName += "#" + instanceCount.ToString();

            }

 

            // Reader .NET CLR Memory Process ID for the given instance to check if

            // it does match our target process

            using (PerformanceCounter counter = new PerformanceCounter(CategoryNetClrMemory, ProcessId,

                   instanceName, true))

            {

 

                long id = 0;

 

                try

                {

                    while (true)

                    {

                        var sample = counter.NextSample();

                        id = sample.RawValue;

 

                        // for some reason it takes quite a while until the counter is

                        // updated with the correct data

                        if (id > 0)

                            break;

 

                        Thread.Sleep(15);

                    }

                }

                catch (InvalidOperationException)

                {

                    // swallow exceptions from non existing instances we tried to read

                }

 

                return (id == p.Id) ? instanceName : null;

            }

 

        }

 

        string GetManagedPerformanceCounterInstanceName(Process p)

        {

            Func<int, Process, string> PidReader = GetInstanceNameForProcess;

            string instanceName = null;

            AutoResetEvent ev = new AutoResetEvent(false);

 

            for (int i = 0; i < ProcessesToTry; i++)

            {

                int tmp = i;

                // Since reading the performance counter for every process is

                // very slow we try to speed up our search by reading up to ProcessesToTry

                // in parallel

                PidReader.BeginInvoke(tmp, p, (IAsyncResult res) =>

                    {

                        if (instanceName == null)

                        {

                           string correctInstanceName = PidReader.EndInvoke(res);

 

                           if (correctInstanceName != null)

                            {

                                instanceName = correctInstanceName;

                                ev.Set();

                            }

                        }

 

                    }, null);

            }

 

 

            // wait until we got the correct instance name or give up

            if (!ev.WaitOne(20 * 1000))

            {

                throw new InvalidOperationException("Could not get managed performance counter instance name for process " + p.Id);

            }

 

            return instanceName;

        }

 

        public PerfCounterReader(Process p)

        {

            string processInstanceName = GetManagedPerformanceCounterInstanceName(p);

            myMemoryCounter = new PerformanceCounter(CategoryNetClrMemory, "# Bytes in all Heaps", processInstanceName);

        }

 

        public long BytesInAllHeaps

        {

            get

            {

                return myMemoryCounter.NextSample().RawValue;

            }

        }

 

        #region IDisposable Members

 

        public void Dispose()

        {

            myMemoryCounter.Dispose();

        }

 

        #endregion

    }

}

To use this class you can give it your current process to check how exact the counter behaves:

 

            var p = Process.GetCurrentProcess();

            using(PerfCounterReader reader = new PerfCounterReader(p))

            {

                while (true)

                {

                    Console.WriteLine("Managed Heap Memory[{0}]: {1:N0} {2:N0}", p.Id, reader.BytesInAllHeaps, GC.GetTotalMemory(false));

                    memory.Add(new List<byte>(10000 * 1000));

                    Thread.Sleep(1000);

                }

            }

 

This will produce output similar to this:

Managed Heap Memory[1616]:     868.844   1.129.604
Managed Heap Memory[1616]:     868.844  11.202.852
Managed Heap Memory[1616]:  10.840.884  20.376.384
Managed Heap Memory[1616]:  20.912.640  30.376.376
Managed Heap Memory[1616]:  20.912.640  40.449.624
Managed Heap Memory[1616]:  20.912.640  50.522.872
Managed Heap Memory[1616]:  20.912.640  60.596.120
Managed Heap Memory[1616]:  20.912.640  70.669.368
Managed Heap Memory[1616]:  20.912.640  80.734.424
Managed Heap Memory[1616]:  20.912.640  90.807.672
Managed Heap Memory[1616]:  20.912.640 100.880.920
Managed Heap Memory[1616]: 100.841.252 110.376.732
Managed Heap Memory[1616]: 100.841.252 120.449.980

What is interesting that the GC.GetTotalMemory function gives much more precise results than the performance counter. It seems that the performance counter is updated only once every 5-10 seconds which is quite slow but better than nothing. The .NET Memory Performance Counters are updated after every GC.Collect. If you want to track during unit tests your resource consumption in a timely manner you will need to add quite big sleeps or trigger a GC in the remote process to get decent reliable numbers.

As a rule of the thumb I can only emphasize measure and check your numbers for errors. Coming from nuclear physics I was educated to question the numbers and check for consistency. This art seems to have gotten lost in our fast paced IT industry where the display (excel sheet with fancy macros) seems to be more important than what you actually did measure. If these numbers do help you to track and steer resource consumption, performance, ... then you have produced real business value. Once you have got reliable measurements you can reason about the numbers what they can tell you. With an increasing amount of work you can

  1. Measure something wrong
  2. Measure something right
  3. Measure the relevant things right
  4. Measure the relevant things right and take further actions to improve your software.

If you are stuck in 1-3 then you have gained nothing for your current project because the knowledge gained from your measurements does not flow back into your software.

Measuring for example the available physical memory before and after a test will show you that you have "lost" or "gained" 100-300 MB of memory. But what does it tell you about the resource consumption of your tests? Not much since the OS does manage your physical memory of all processes. Even if you have a big memory leak it does not necessarily show up a lost physical memory since the OS is quite good at paging unused memory out into the pagefile. The machine wide memory consumption is easy to measure but of little value (2). More about the Zen of measuring performance/consumption right is the topic of a future post.

  • Share This Post:
  • Share on Twitter
  • Share on Facebook
  • Share on Technorati
Posted On Wednesday, May 27, 2009 11:02 AM | Feedback (2)
DEVPATH Is Back!

Uhh What? DevPath is an environment variable that allows you specify global directories which are searched just like GAC. If you ever had the urge to load dlls from your application from subdirectories you need a probing element in your app.config which allows exactly that.

The only problem with that is that you cannot escape from your application root directory. When you try to load something from ..\Centralbin it is ignored. In that cases you need to use the GAC if you like it or not. Since DevPath was broken for some time with .NET 2.0 I thought it was no longer supported. But thanks to John Robbins article "PDB Files: What every Developer must know." I did learn a different story. That makes it possibly now to use some Microsoft tools in a standard fashion. The Xml Serialization assembly generator Sgen for example can create the serialization assembly only if all public serializable types do not have dependencies to assemblies in other directories. This is a major PITA since fresh compiled assemblies are located in other directories than the rest (except it you have set Copy To Local to true in Visual Studio but that is a bad idea either).

But now we can alter the sgen.exe.config and add one line

<?xml version ="1.0"?>
<configuration>
    <runtime>       
        <generatePublisherEvidence enabled="false"/>
        <developmentMode developerInstallation="true"/>
    </runtime>
</configuration>

And now behold lets call sgen.exe

C:\Source>sgen.exe

System.Threading.SynchronizationLockException: Object synchronization method was called from an unsynchronized block of code.
at System.Resources.ResourceManager.TryLookingForSatellite(CultureInfo lookForCulture)
at System.Resources.ResourceManager.InternalGetResourceSet(CultureInfo culture, Boolean createIfNotExists, Boolean tryParents)
at System.Resources.ResourceManager.GetString(String name, CultureInfo culture)
at System.Environment.ResourceHelper.GetResourceStringCode(Object userDataIn)
at System.Environment.GetResourceFromDefault(String key)
at System.Environment.GetResourceString(String key)
at System.IO.Path.CheckInvalidPathChars(String path)
at System.IO.Path.NormalizePathFast(String path, Boolean fullCheck)
at System.IO.Path.NormalizePath(String path, Boolean fullCheck)
at System.IO.Path.GetFullPathInternal(String path)
at System.AppDomainSetup.set_DeveloperPath(String value)
at System.AppDomain.SetupFusionStore(AppDomainSetup info)
at System.AppDomain.SetupDomain(Boolean allowRedirects, String path, String configFile)

Ups. .NET 3.5 SP1 did not do the trick? Some investigation shows that the .NET Framework is still subject to bad error handling. If DEVPATH is empty or DEVPATH contains invalid path characters such as > < | or " then it will try to report the issue so far so good. But it seems that Microsoft seems to be a lover of fast in process tests where each methods works perfectly but the whole thing blows apart when used in a true product scenario. This is not the first time that did happen with DEVPATH but I thought that since the release of .NET 2.0 in 2005 these things would have been fixed and some regression tests had been added. Apparently I was wrong.

In my specific case I did try set devpath="c:\Source\EntLib3Src\App Blocks\bin" which did fail because of the parenthesis. Once I removed the "invalid" characters all did work out fine.

During my investigation with Reflector I stumbled upon another undocumented environment variable RELPATH which does set the private probing path.

info.PrivateBinPath = Environment.nativeGetEnvironmentVariable(AppDomainSetup.PrivateBinPathEnvironmentVariable);

When I set it to e.g. subDir then I do no longer need to set the private probing path in my App.config. Nice that could come in handy in some scenarios.

  • Share This Post:
  • Share on Twitter
  • Share on Facebook
  • Share on Technorati
Posted On Thursday, May 14, 2009 7:56 PM | Feedback (2)