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);
 
        }
    }
}
 

Thursday, December 4, 2008

Regasm and GAC fun (MSB3217 and RA0000)

Problem: A project builds an assembly successfully but An indicates an error similar to the following:
C:\Windows\Microsoft.NET\Framework\v3.5\Microsoft.Common.targets(2937,9): error MSB3217: Cannot register assembly "C:\Source\Blog\RegasmExamples2\bin\Debug\RegasmExamples2.dll". Method 'SomeProperty' in type 'RegasmExamples2.BrokenClass' from assembly 'RegasmExamples2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.
When using the "Register for COM interop" option in a .Net project

Or

RegAsm : error RA0000: Method 'get_SomeOtherProperty' in type 'RegasmExamples2.BrokenClass2' from assembly 'RegasmExamples2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=72d8f57e6c124ae0' does not have an implementation.C:\Windows\Microsoft.NET\Framework\v3.5\Microsoft.Common.targets(3314,13): error MSB3073: The command "C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\regasm C:\Source\Blog\RegasmExamples\RegasmExamples2\bin\Debug\RegasmExamples2.dll" exited with code 100.
When using Regasm as a post build step in a .Net Project


Common Causes:

  • An interop assembly is in the GAC that has a different interface definition than the one that was referenced and built against but they have the same strong name. Remove the incorrect version of the interop from the GAC and identify the correct version of the interop and install it in the GAC if required.
  • An older version of the project's target assembly is installed in the GAC that does not implement the new interface definition but has the same strong name. Remove the build target from the GAC either manually or with a pre-build step.

Notes:

  • The location of the assembly that is used when building is defined by the reference and may be different than the assembly probing sequence at runtime.
  • Regasm is a .Net application and will follow the standard assembly probing sequence. If an older build of the assembly is in the GAC (with the same assembly version, culture, and architecture) then it will use that binary.
  • This problem occurs most commonly when someone is monkeying around with interfaces (typically when "fleshing" them out) or as a regression when building mutliple versions of a product on a development machine and someone decided to make an interface "better" instead of extending it with a new one.

Tools to Diagnose:

  • File Monitor or ProcessMonitor by Sysinternals to monitor the build and registration
  • Reflector to view the interface definition of the assembly in the GAC