Skip to content

rob-blackbourn/JetBlack.Diagnostics

Repository files navigation

JetBlack.Diagnostics

Introduction

This library provides some support code for supplementing the standard System.Diagnostics assembly.

Performance counters

A framework for performance counters addresses three issues: ease of development, testability, usability.

Ease of development & testability

When writing code which contains performance counters, development is hampered by having to register performance counters before debugging the code. The same problem arises in testing. This issue is addressed by creating an interface IPerformanceCounter, a factory IPerformanceCounterFactory, and a concrete implementation of the actual performance counter. This means that in development and test we can mock the counters before installing them in production.

Usability

Under the hood a performance counter is essentially a long, a timestamp, and a type. The type determines the calculation performed when the counter is sampled. The system also understands how to combine two counters of specific types (a counter and a base) to provide more sophisticated statics; like average time spent performing an operation. They need to be registered before use; and in the case of composite counters we must ensure the counter and base are registered consequtively and in order.

An example of this is the average timer. It consists of a numerator of type AverageCouter32 and a denominator of AverageBase. The average time is achieved by incrementing the numerator by the elapsed ticks, and the denominator by one. When registered we must ensure that the numerator is created before the denominator which must immediately succeed it.

The AverageTimer class provides this functionality. The increment method with the supplied elapsed ticks updates the numerator by the ticks an the denominator by one. A static method can create the counter data for an installer. As the method uses a factory interface to create it's counters, development and testing are not hampered by the need for registration.

Example

If we imagine a lazy cache, we might want to monitor the number of times a fetch occurs, and the average time it takes to do the fetch. The monitor class might look as follows.

public class CacheMonitor
{
    public const string CountSuffix = "Count";
    public const string AverageFetchSuffix = "AverageFetch";

    public CacheMonitor(IPerformanceCounterFactory factory, string categoryName, string cacheName, bool readOnly)
    {
        Count = new NumberOfItems32(factory, categoryName, cacheName + "Count", readOnly);
        AverageFetch = new AverageTimer(factory, categoryName, cacheName + "AverageFetch", readOnly);

        if (!readOnly)
            Reset();
    }

    public NumberOfItems32 Count { get; private set; }
    public AverageTimer AverageFetch { get; private set; }

    public void Reset()
    {
        Count.Reset();
        AverageFetch.Reset();
    }

    public static CounterCreationData[] CreateCounterData(string cacheName)
    {
        return NumberOfItems32.CounterCreator.CreateCounterData(cacheName + CountSuffix, "The number of times the cache has been accessed")
            .Concat(AverageTimer.CounterCreator.CreateCounterData(cacheName + AverageFetchSuffix, "The average time taken to fetch an item from the cache"))
            .ToArray();
    }
}

Note that a counter factory is used to generate the counter to provide mock support. We have bundled the counters required, one of which is actually a composite counter (so in reality three counters are required). The CreateCounterData method consolidates the data required to install the counters.

The cache might be implemented as follows.

public delegate bool TryGetValue<TKey, TValue>(TKey key, out TValue value);

    public class Cache<TKey, TValue>
    {
        private readonly IDictionary<TKey, TValue> _cache = new Dictionary<TKey, TValue>();
        private readonly TryGetValue<TKey, TValue> _tryGetValue;
        private readonly CacheMonitor _monitor;

        public Cache(TryGetValue<TKey, TValue> tryGetValue, CacheMonitor monitor)
        {
            _tryGetValue = tryGetValue;
            _monitor = monitor;
        }
        
        public bool TryGetValue(TKey key, out TValue value)
        {
            if (_cache.TryGetValue(key, out value))
                return true;

            var stopwatch = new Stopwatch();
            try
            {
                stopwatch.Start();
                if (!_tryGetValue(key, out value))
                    return false;
                _cache.Add(key, value);
                return true;
            }
            finally
            {
                stopwatch.Stop();
                _monitor.AverageFetch.Increment(stopwatch.ElapsedTicks);
                _monitor.Count.Increment();
            }
        }
    }

And an installer for a mythical user cache could be implemented in the following manner.

public class CacheInstaller : Installer
{
    public CacheInstaller()
    {
        var installer = new PerformanceCounterInstaller
        {
            CategoryName = "Example User Cache",
            CategoryHelp = "An example cache of users.",
            CategoryType = PerformanceCounterCategoryType.SingleInstance,
            UninstallAction = UninstallAction.Remove
        };
        installer.Counters.AddRange(CacheMonitor.CreateCounterData("UserCache"));

        Installers.Add(installer);
    }
}

About

Performance counters

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages