Performance counters in C# made easier
If you ever got to tango with performance counters in C# (+1, they can be very useful) you might have noticed that the current API leaves much to be desired. Things can get even more cluttered when you have a bunch of them. You have to store the name, help string, and type for each one so you can feed it to the API later on. You'll also need to store an instance for each counter so you can have its value updated. The other day a colleague of mine mentioned that the task of adding a new counter becomes a chore.
The good news is, you can make your life easier.
The mechanism that will assist you in doing so consists of the following key elements:
- An enum representing the performance counters we're going to have. Each counter will have a corresponding member in this enum, future counters will be added here as well.
- A set of attributes to decorate the above mentioned enum and provide a declarative way of conveying counter metadata.
- Some extension methods to alleviate metadata retrieval and make things cleaner.
- A class tying it all up in a neat API.
The attributes look like so:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// An attribute describing a performance counter's name. | |
/// </summary> | |
public class CounterNameAttribute : Attribute | |
{ | |
/// <summary> | |
/// Initializes a new instance of the <see cref="CounterNameAttribute"/> class. | |
/// </summary> | |
/// <param name="counterName"> | |
/// The counter name. | |
/// </param> | |
public CounterNameAttribute(string counterName) | |
{ | |
CounterName = counterName; | |
} | |
/// <summary> | |
/// Gets or sets the counter name info. | |
/// </summary> | |
public string CounterName { get; set; } | |
} | |
/// <summary> | |
/// An attribute describing a performance counter's help string. | |
/// </summary> | |
public class HelpStringAttribute : Attribute | |
{ | |
/// <summary> | |
/// Initializes a new instance of the <see cref="HelpStringAttribute"/> class. | |
/// </summary> | |
/// <param name="helpString"> | |
/// The performance counter's help string. | |
/// </param> | |
public HelpStringAttribute(string helpString) | |
{ | |
HelpString = helpString; | |
} | |
/// <summary> | |
/// Gets or sets the performance counter's help string info. | |
/// </summary> | |
public string HelpString { get; set; } | |
} | |
/// <summary> | |
/// An attribute describing a performance counter's type. | |
/// </summary> | |
public class CounterTypeAttribute : Attribute | |
{ | |
/// <summary> | |
/// Initializes a new instance of the <see cref="CounterTypeAttribute"/> class. | |
/// </summary> | |
/// <param name="counterType"> | |
/// The performance counter Type. | |
/// </param> | |
public CounterTypeAttribute(PerformanceCounterType counterType) | |
{ | |
CounterType = counterType; | |
} | |
/// <summary> | |
/// Gets or sets the counter type info. | |
/// </summary> | |
public PerformanceCounterType CounterType { get; set; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// The enum representing the performance counter metadata. | |
/// </summary> | |
public enum PerformanceCounterMetaData | |
{ | |
/// <summary> | |
/// An entry representing the performance counter for the whatever items. | |
/// </summary> | |
[CounterName("My counter name")] | |
[CounterType(PerformanceCounterType.NumberOfItems64)] | |
[HelpString("The number of whatever items")] | |
MY_COUNTER_NAME, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// Provides some extension method for the PerformanceCounterMetaData type. | |
/// </summary> | |
public static class PerformanceCounterMetaDataExtensions | |
{ | |
/// <summary> | |
/// An extension method that gets the help string from a PerformanceCounterMetaData member. | |
/// </summary> | |
/// <param name="counterMetaData"> | |
/// The PerformanceCounterMetaData member to get the help string from. | |
/// </param> | |
/// <returns> | |
/// The help string for the given counter meta data. | |
/// </returns> | |
public static string GetHelpString(this PerformanceCounterMetaData counterMetaData) | |
{ | |
var helpStringAttr = GetAttribute<HelpStringAttribute>(counterMetaData); | |
return helpStringAttr.HelpString; | |
} | |
/// <summary> | |
/// An extension method that gets the counter type from a PerformanceCounterMetaData member. | |
/// </summary> | |
/// <param name="counterMetaData"> | |
/// The PerformanceCounterMetaData member to get the counter type from. | |
/// </param> | |
/// <returns> | |
/// The type of the given counter meta data. | |
/// </returns> | |
public static PerformanceCounterType GetCounterType(this PerformanceCounterMetaData counterMetaData) | |
{ | |
// extract the type attribute | |
var typeAttr = GetAttribute<CounterTypeAttribute>(counterMetaData); | |
return typeAttr.CounterType; | |
} | |
/// <summary> | |
/// An extension method that gets the counter name from a PerformanceCounterMetaData member. | |
/// </summary> | |
/// <param name="counterMetaData"> | |
/// The PerformanceCounterMetaData member to get the name from. | |
/// </param> | |
/// <returns> | |
/// The counter name for the given counter meta data. | |
/// </returns> | |
public static string GetCounterName(this PerformanceCounterMetaData counterMetaData) | |
{ | |
// extract the type attribute | |
var nameAttr = GetAttribute<CounterNameAttribute>(counterMetaData); | |
return nameAttr.CounterName; | |
} | |
/// <summary> | |
/// An extension method that creates a counter creation data for a PerformanceCounterMetaData member. | |
/// </summary> | |
/// <param name="counterMetaData"> | |
/// The PerformanceCounterMetaData member to create the counter creation data for. | |
/// </param> | |
/// <returns> | |
/// The CounterCreationData for the given counter meta data. | |
/// </returns> | |
public static CounterCreationData GetCounterCreationData(this PerformanceCounterMetaData counterMetaData) | |
{ | |
var name = counterMetaData.GetCounterName(); | |
var type = counterMetaData.GetCounterType(); | |
var helpString = counterMetaData.GetHelpString(); | |
return new CounterCreationData(name, helpString, type); | |
} | |
/// <summary> | |
/// An extension method that creates a PerformanceCounter instance for a PerformanceCounterMetaData member. | |
/// </summary> | |
/// <param name="counterMetaData"> | |
/// The PerformanceCounterMetaData member to create the PerformanceCounter for. | |
/// </param> | |
/// <param name="categoryName"> | |
/// The performance counter category Name. | |
/// </param> | |
/// <returns> | |
/// The PerformanceCounter created for the given PerformanceCounterMetaData member. | |
/// </returns> | |
public static PerformanceCounter GetPerformanceCounter( | |
this PerformanceCounterMetaData counterMetaData, | |
string categoryName) | |
{ | |
return | |
new PerformanceCounter( | |
categoryName, | |
counterMetaData.GetCounterName(), | |
false); | |
} | |
/// <summary> | |
/// A utility method the extracts a custom attribute from a PerformanceCounterMetaData instance and casts it to the | |
/// right type. | |
/// </summary> | |
/// <param name="counterMetaData"> | |
/// The PerformanceCounterMetaData instance to extract the custom attribute from. | |
/// </param> | |
/// <typeparam name="T">The type of the attribute to be extracted. | |
/// </typeparam> | |
/// <returns> | |
/// The custom attribute extracted from the given PerformanceCounterMetaData instance. | |
/// </returns> | |
private static T GetAttribute<T>(PerformanceCounterMetaData counterMetaData) where T : Attribute | |
{ | |
var enumMemberInfo = typeof(PerformanceCounterMetaData).GetMember(counterMetaData.ToString()).Single(); | |
return (T)Attribute.GetCustomAttribute(enumMemberInfo, typeof(T)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// Provides the infrastructure for working with performance counters using PerformanceCounterMetaData. | |
/// </summary> | |
public class PerformanceCounters | |
{ | |
/// <summary> | |
/// The performance counter category name. | |
/// </summary> | |
private readonly string _categoryName; | |
/// <summary> | |
/// The performance counter category help string. | |
/// </summary> | |
private readonly string _categoryHelp; | |
/// <summary> | |
/// A mapping between the counter meta data and an actual performance counter instance. | |
/// </summary> | |
private Dictionary<PerformanceCounterMetaData, PerformanceCounter> _metaDataToCounter; | |
/// <summary> | |
/// Initializes a new instance of the <see cref="PerformanceCounters"/> class. | |
/// </summary> | |
/// <param name="categoryName"> | |
/// The category name string. | |
/// </param> | |
/// <param name="categoryHelp"> | |
/// The category help string. | |
/// </param> | |
public PerformanceCounters(string categoryName, string categoryHelp) | |
{ | |
_categoryName = categoryName; | |
_categoryHelp = categoryHelp; | |
} | |
/// <summary> | |
/// Creates the performance counter category and counters. | |
/// </summary> | |
public void CreateCounters() | |
{ | |
if (PerformanceCounterCategory.Exists(_categoryName)) | |
{ | |
PerformanceCounterCategory.Delete(_categoryName); | |
} | |
// we need to iterate over all enum members. The way to do it here is to get names of all ENUM members | |
// as strings, and convert them back to their corresponding enum member | |
var typedPerfCountMetadataEnumMembers = | |
Enum.GetNames(typeof(PerformanceCounterMetaData)) | |
.Select(member => (PerformanceCounterMetaData)Enum.Parse(typeof(PerformanceCounterMetaData), member)) | |
.ToList(); | |
var counterCreationDataCollection = | |
new CounterCreationDataCollection( | |
typedPerfCountMetadataEnumMembers | |
.Select(counterMetaData => counterMetaData.GetCounterCreationData()) | |
.ToArray()); | |
// create all performance counters | |
PerformanceCounterCategory.Create( | |
_categoryName, | |
_categoryHelp, | |
PerformanceCounterCategoryType.SingleInstance, | |
counterCreationDataCollection); | |
// We need a reference to the actual counter instance in order to update it enter the meta data to performance counter instance mapping | |
_metaDataToCounter = typedPerfCountMetadataEnumMembers.ToDictionary( | |
perfCountMetadata => perfCountMetadata, | |
perfCountMetadata => perfCountMetadata.GetPerformanceCounter(_categoryName)); | |
} | |
/// <summary> | |
/// Gets the performance counter instance corresponding with a metadata object. | |
/// </summary> | |
/// <param name="metaData"> | |
/// The meta data object describing a performance counter. | |
/// </param> | |
/// <returns> | |
/// The performance counter instance corresponding with the provided metadata object. | |
/// </returns> | |
/// <exception cref="InvalidOperationException"> | |
/// Thrown in case the counters have not been created yet - need to check if <see cref="CreateCounters"/> was called by client | |
/// </exception> | |
public PerformanceCounter GetCounter(PerformanceCounterMetaData metaData) | |
{ | |
PerformanceCounter theCounter; | |
if (!_metaDataToCounter.TryGetValue(metaData, out theCounter)) | |
{ | |
throw new InvalidOperationException("The counters have not been created yet"); | |
} | |
return theCounter; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* using our new performance counter API */ | |
// initiate a PerformanceCounters instance | |
PerformanceCounters _perfCounters = new PerformanceCounters(CATEGORY_NAME, CATEGORY_NAME_HELP); | |
// have ALL your counters created | |
_perfCounters.CreateCounters(); | |
// update the counter value | |
_perfCounters.Get(PerformanceCounterMetaData.MY_COUNTER_NAME).Increment(); |
This mechanism has some nice benefits:
- Adding a new counter basically boils down to adding a new member to the enum (and decorating it accordingly). Since all enum members are iterated over automatically, it will be added to your category with no further effort on your side.
- All information pertaining to a particular counter is grouped together and is thus easily conveyed to the reader (no more constants representing counter names scattered all around, no more "wait, I can see the name but what was this counter's type again?").
- With further work it can be leveraged into an infrastructural module applicable to any client system (which is not the case at the moment, since the enum is hard-coded with particular client's counters, making it unusable for other clients out of the box) - see the epilogue for more details.
Epilogue
This mechanism can be further improved by eliminating the coupling to a particular enum (i.e., to a particular client), and basically turned into a generic infrastructure component. The keen reader may have noticed that the extension methods are defined on the counters enum, which is client specific - not a fun fact at all.
The direction I had in mind was turning the enum into an interface with get properties decorated pretty much same manner as the current enum. Using reflection we could have a mapping between the property names and actual counter instances. Then we could use castel's DictionaryAdapter to implement that interface and have an object with actual properties which use an underlying mapping (string => counter) for the get / set functionality. The extension methods would be defined on a marker interface (i.e., an empty interface), so that any interfaces down the hierarchy could get them for free. This should allow for any client to define an interface deriving from this marker interface, define its own properties, and get all the perks we've mentioned right out of the box.
So how bout them apples?
Comments
Post a Comment