Alois Kraus

blog

  Home  |   Contact  |   Syndication    |   Login
  107 Posts | 8 Stories | 295 Comments | 162 Trackbacks

News



Article Categories

Archives

Post Categories

Image Galleries

Programming

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.

posted on Wednesday, May 27, 2009 11:02 AM

Feedback

# re: How To Read .NET Performance Counters Correctly 5/28/2009 1:41 AM Sasha Goldshtein
AFAIK the GC performance counters are updated only when a GC occurs. Therefore, it shouldn't be surprising that GC.GetTotalMemory is "more accurate".

# re: How To Read .NET Performance Counters Correctly 5/28/2009 5:56 PM Alois Kraus
Thanks for pointing this out Sasha. That makes it even more trickier to measure the current managed heap from the outside.

Post A Comment
Title:
Name:
Email:
Comment:
Verification: