Wednesday, January 7, 2009

How to use C# to read performance data from a log file

Problem:  The counter classes provided by .Net framework are useful for reading and writing live counter values but do not appear to be able to bind to performance logs.

Soltuion: Use the PDH API via P/Invoke. Performance Monitoring in MSDN provides all of the basic information on the PDH API and some samples in C++. 

Notes:
  • Some counters require multiple samples to be calculated.  PdhCollectQueryData will need to be called multiple times prior to PdhGetFormattedCounterValue in that case.  This demo is using Processor \% Processor Time which requires two samples.
  • Multiple log files can be queried together using PdhBindInputDataSource along with the API functions ending in "H".
  • Use System.Runtime.Interop.Marshal to unpack strings from PDH_FMT_COUNTERVALUE if required.
The following is a simple console application that demonstrates using the API in C# and is a basic port of the example provided in MSDN.  

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Win32.SafeHandles;
using System.Security.Permissions;
using System.Security;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
 
namespace PdhLogDemo
{
 
    #region PDH Definitions
    /// 
    /// A safe wrapper around a PDH Log handle.
    /// 
    /// 
    /// Use this along with PdhBindInputDataSource and the "H" APIs to bind multiple logs together
    /// 
    [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
    public class PdhLogHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        public PdhLogHandle() : base(true)
        {
 
        }
 
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        protected override bool ReleaseHandle()
        {
            return PdhApi.PdhCloseLog(handle, PdhApi.PDH_FLAGS_CLOSE_QUERY) == 0;
        }
    }
 
    /// 
    /// A safe wrapper arounda query handle
    /// 
    [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
    public class PdhQueryHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        public PdhQueryHandle()
            : base(true)
        {
 
        }
 
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        protected override bool ReleaseHandle()
        {
            return PdhApi.PdhCloseQuery(handle) == 0;
        }
    }
 
    /// 
    /// A safe handle around a counter
    /// 
    [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
    public class PdhCounterHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        public PdhCounterHandle()
            : base(true)
        {
 
        }
 
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        protected override bool ReleaseHandle()
        {
            return PdhApi.PdhRemoveCounter(handle) == 0;
        }
    }
 
    /// 
    /// The value of a counter as returned by  API.
    /// 
    /// In C/C++ this is a union.
    [StructLayout(LayoutKind.Explicit)]
    public struct PDH_FMT_COUNTERVALUE
    {
        [FieldOffset(0)]
        public UInt32 CStatus;
        [FieldOffset(1)]
        public int longValue;
        [FieldOffset(1)]
        public double doubleValue;
        [FieldOffset(1)]
        public long longLongValue;
        [FieldOffset(1)]
        public IntPtr AnsiStringValue;
        [FieldOffset(1)]
        public IntPtr WideStringValue;
    }
 
    /// 
    /// The requested format for the  API.
    /// 
    [Flags()]
    public enum PdhFormat : uint 
    {
        PDH_FMT_RAW = 0x00000010,
        PDH_FMT_ANSI = 0x00000020,
        PDH_FMT_UNICODE = 0x00000040,
        PDH_FMT_LONG = 0x00000100,
        PDH_FMT_DOUBLE = 0x00000200,
        PDH_FMT_LARGE = 0x00000400,
        PDH_FMT_NOSCALE = 0x00001000,
        PDH_FMT_1000 = 0x00002000,
        PDH_FMT_NODATA = 0x00004000
    }
 
    /// 
    /// Static class containing some usefull PDH API's
    /// 
    [SuppressUnmanagedCodeSecurity()]
    internal class PdhApi
    {
        #region A few common flags and status codes
        public const UInt32 PDH_FLAGS_CLOSE_QUERY = 1;
        public const UInt32 PDH_NO_MORE_DATA = 0xC0000BCC;
        public const UInt32 PDH_INVALID_DATA = 0xC0000BC6;
        public const UInt32 PDH_ENTRY_NOT_IN_LOG_FILE = 0xC0000BCD;
        #endregion
 
        /// 
        /// Opens a query handle
        /// 
        /// 
        /// 
        /// 
        /// 
        [DllImport("pdh.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern UInt32 PdhOpenQuery(
         string szDataSource,
         IntPtr dwUserData,
         out  PdhQueryHandle phQuery);
 
        /// 
        /// Opens a query against a bound input source.
        /// 
        /// 
        /// 
        /// 
        /// 
        [DllImport("pdh.dll", SetLastError = true)]
        public static extern UInt32 PdhOpenQueryH(
         PdhLogHandle hDataSource,
         IntPtr dwUserData,
         out  PdhQueryHandle phQuery);
 
        /// 
        /// Binds multiple logs files together.
        /// 
        /// Use this along with the API's ending in 'H' to string multiple files together.
        /// 
        /// 
        /// 
        [DllImport("pdh.dll", SetLastError = true)]
        public static extern UInt32 PdhBindInputDataSource(
            out PdhLogHandle phDataSource,
            string szLogFileNameList);
 
        /// 
        /// Closes a handle to a log
        /// 
        /// 
        /// 
        /// 
        [DllImport("pdh.dll", SetLastError = true)]
        public static extern UInt32 PdhCloseLog(
            IntPtr hLog,
            long dwFlags);
 
        /// 
        /// Closes a handle to the log
        /// 
        /// 
        /// 
        [DllImport("pdh.dll", SetLastError = true)]
        public static extern UInt32 PdhCloseQuery(
            IntPtr hQuery);
 
        /// 
        /// Removes a counter from the given query.
        /// 
        /// 
        /// 
        [DllImport("pdh.dll", SetLastError = true)]
        public static extern UInt32 PdhRemoveCounter(
            IntPtr hQuery);
 
        /// 
        /// Adds a counter the query and passes out a handle to the counter.
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        [DllImport("pdh.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern UInt32 PdhAddCounter(
            PdhQueryHandle hQuery,
            string szFullCounterPath,
            IntPtr dwUserData,
            out PdhCounterHandle phCounter);
 
        /// 
        /// Retrieves a sample from the source.
        /// 
        /// 
        /// 
        [DllImport("pdh.dll", SetLastError = true)]
        public static extern UInt32 PdhCollectQueryData(
            PdhQueryHandle phQuery);
 
        /// 
        /// Retrieves a specific counter value in the specified format.
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        [DllImport("pdh.dll", SetLastError = true)]
        public static extern UInt32 PdhGetFormattedCounterValue(
            PdhCounterHandle phCounter,
            PdhFormat dwFormat,
            IntPtr lpdwType,
            out PDH_FMT_COUNTERVALUE pValue);
    }
    #endregion
 
 
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length != 1)
            {
                Console.WriteLine("PdhLogDemo is a simple C# port of the sample application describe in the PDH API MSDN help.");
                Console.WriteLine("The sample takes a single argument which should be the path to perf log containing at least two samples");
                Console.WriteLine(@"of the counter \Processor(0)\% Processor Time");
                return;
            }
 
            //the name of the counter
            string counterName = @"\Processor(0)\% Processor Time";
            //the location of the log
            string log = args[0]; //@"C:\perflogs\Admin\New Data Collector Set\000002\New Data Collector.blg";
            PdhQueryHandle query;
            PdhCounterHandle counter;
 
            UInt32 result = PdhApi.PdhOpenQuery(log, IntPtr.Zero, out query);
            if (result != 0)
            {
                Console.WriteLine("Unable to open query.  Return was: " + result.ToString());
                return;
            }
 
            //Add the counter that we want to see
            //The PdhEnumXXX methods can be used to discover them
            result = PdhApi.PdhAddCounter(query, counterName, IntPtr.Zero, out counter);
            if (result != 0)
            {
                //0xC0000BCD (PDH_ENTRY_NOT_IN_LOG_FILE) means the counter was not found in the set
                Console.WriteLine("Unable to add counter.  Return was: " + result.ToString());
                return;
            }
 
            //Since we're using % processor time which requires two samples call this once initially to pull the first sample
            result = PdhApi.PdhCollectQueryData(query);
            if (result == PdhApi.PDH_NO_MORE_DATA)
            {
                Console.WriteLine("The log file contains no data.");
                return;
            }
 
            do
            {
                result = PdhApi.PdhCollectQueryData(query);
                if (result == 0)
                {
                    PDH_FMT_COUNTERVALUE value;
                    result = PdhApi.PdhGetFormattedCounterValue(counter, PdhFormat.PDH_FMT_DOUBLE, IntPtr.Zero, out value);
                    //Check CStatus to make sure it is ok
                    //0xC0000BC6 (PDH_INVALID_DATA) can be returned when more samples are needed to calculate the value.  Call PdhCollectQueryData
                    if (value.CStatus == 0)
                    {
                        Console.WriteLine("Value = " + value.doubleValue.ToString());
                    }
                    else
                    {
                        Console.WriteLine("Bad CStatus on Value: " + value.CStatus.ToString());
                    }
                }
            }
            while (result == 0);
 
        }
    }
}