Skip to content
Snippets Groups Projects
PhysicalDisk.cs 9.04 KiB
Newer Older
using System;
using System.Diagnostics;
using System.Linq;
// using System.Threading;
using System.Timers;
using NLog;
using Timer = System.Timers.Timer;

namespace ATxCommon.Monitoring
{
    /// <summary>
    /// Load monitoring class for physical disks, constantly checking the queue length at the
    /// given <see cref="Interval"/> in a separate (timer-based) thread.
    /// 
    /// The load (=queue length) is determined using a <see cref="PerformanceCounter"/>, and is
    /// compared against a configurable <see cref="Limit"/>. If the load changes from below the
    /// limit to above, a <see cref="LoadAboveLimit"/> event will be raised. If the load has been
    /// above the limit and is then dropping below, an <see cref="OnLoadBelowLimit"/> event will
    /// be raised as soon as a given number of consecutive load measurements (defined via <see
    /// cref="Probation"/>) were found to be below the limit.
    /// </summary>
    public class PhysicalDisk
    {
        private static readonly Logger Log = LogManager.GetCurrentClassLogger();

        /// <summary>
        /// The generic event handler delegate for PhysicalDisk events.
        /// </summary>
        public delegate void EventHandler(object sender, EventArgs e);
        
        /// <summary>
        /// Event raised when the PhysicalDisk load exceeds the limit for any measurement.
        /// </summary>
        public event EventHandler LoadAboveLimit;

        /// <summary>
        /// Event raised when the PhysicalDisk load is below the configured limit for at least
        /// the number of consecutive measurements configured in <see cref="Probation"/> after
        /// having exceeded this limit before.
        /// </summary>
        public event EventHandler LoadBelowLimit;

        private readonly Timer _monitoringTimer;
        private readonly PerformanceCounter _diskQueueLength;
        private readonly float[] _loadReadings = {0F, 0F, 0F, 0F};

        private int _interval;
        private int _behaving;
        private int _probation;

        private float _limit;



        #region properties

        /// <summary>
        /// Current PhysicalDisk Queue Length, averaged of the last four readings.
        /// </summary>
        /// <returns>The average PhysicalDisk Queue Length from the last four readings.</returns>
        public float Load { get; private set; }

        /// <summary>
        /// Flag representing whether the load is considered to be high or low.
        /// </summary>
        public bool HighLoad { get; private set; }

        /// <summary>
        /// How often (in ms) to check the PhysicalDisk Queue Length.
        /// </summary>
        public int Interval {
            get => _interval;
            set {
                _interval = value;
                _monitoringTimer.Interval = value;
                Log.Debug("PhysicalDisk Queue Length monitoring interval: {0}", _interval);
            }
        }

        /// <summary>
        /// Upper limit of PhysicalDisk load before it is classified as "high".
        /// </summary>
        public float Limit {
            get => _limit;
            set {
                _limit = value;
                Log.Debug("PhysicalDisk monitoring limit: {0:0.000}", _limit);
            }
        }

        /// <summary>
        /// Number of cycles where the PhysicalDisk load value has to be below the limit before it is
        /// classified as "low" again.
        /// </summary>
        public int Probation {
            get => _probation;
            set {
                _probation = value;
                Log.Debug("PhysicalDisk monitoring probation cycles when violating limit: {0}", _probation);
            }
        }
        
        /// <summary>
        /// Indicating whether the PhysicalDisk load monitoring is active.
        /// </summary>
        public bool Enabled {
            get => _monitoringTimer.Enabled;
            set {
                Log.Debug("{0} PhysicalDisk monitoring.", value ? "Enabling" : "Disabling");
                _monitoringTimer.Enabled = value;
            }
        }

        /// <summary>
        /// Log level to use for reporting current performance readings.
        /// </summary>
        public LogLevel LogPerformanceReadings { get; set; } = LogLevel.Trace;

        /// <summary>
        /// Create performance counter and initialize it.
        /// </summary>
        /// <param name="counterName">The counter to use for the monitoring, default is the
        /// overall "Avg. Disk Queue Length", other reasonable options are the corresponding read
        /// or write queues ("Avg. Disk Read Queue Length" and "Avg. Disk Write Queue Length").
        /// </param>
        public PhysicalDisk(string counterName = "Avg. Disk Queue Length") {
            _interval = 250;
            _limit = 0.5F;
            _probation = 40;
            Log.Info($"Initializing PhysicalDisk performance monitoring for [{counterName}]...");
            try {
                Log.Debug("PhysicalDisk monitoring initializing PerformanceCounter...");
                _diskQueueLength = new PerformanceCounter("PhysicalDisk", counterName, "_Total");
                var curLoad = _diskQueueLength.NextValue();
                Log.Debug("PhysicalDisk Queue Length initial value: {0:0.000}", curLoad);
                /* this initialization doesn't seem to be necessary for PhysicalDisk, so we just
                 * disable those calls for now:
                Thread.Sleep(1000);
                curLoad = _diskQueueLength.NextValue();
                Log.Debug("PhysicalDisk monitoring current queue length: {0:0.000}", curLoad);
                 */
                // now initialize the load state:
                HighLoad = curLoad > _limit;
                _monitoringTimer = new Timer(_interval);
                _monitoringTimer.Elapsed += UpdatePhysicalDiskLoad;
            }
            catch (Exception) {
                Log.Error("Initializing PhysicalDisk monitoring failed!");
                throw;
            }

            Log.Debug("Initializing PhysicalDisk monitoring completed.");
        }

        /// <summary>
        /// Check current PhysicalDisk queue length, update the history of readings and trigger
        /// the corresponding events if the required criteria are met.
        /// </summary>
        private void UpdatePhysicalDiskLoad(object sender, ElapsedEventArgs e) {
            _monitoringTimer.Enabled = false;
            try {
                // ConstrainedCopy seems to be the most efficient approach to shift the array:
                Array.ConstrainedCopy(_loadReadings, 1, _loadReadings, 0, 3);
                _loadReadings[3] = _diskQueueLength.NextValue();
                Load = _loadReadings.Average();
                if (_loadReadings[3] > _limit) {
                    if (_behaving > _probation) {
                        // this means the load was considered as "low" before, so raise an event:
                        OnLoadAboveLimit();
                        Log.Trace("PhysicalDisk Queue Length ({0:0.00}) violating limit ({1})!",
                            _loadReadings[3], _limit);
                    } else if (_behaving > 0) {
                        // this means we were still in probation, so no need to trigger again...
                        Log.Trace("PhysicalDisk: resetting behaving counter (was {0}).", _behaving);
                    }
                    _behaving = 0;
                } else {
                    _behaving++;
                    if (_behaving == _probation) {
                        Log.Trace("PhysicalDisk Queue Length below limit for {0} cycles, " +
                                  "passing probation!", _probation);
                        OnLoadBelowLimit();
                    } else if (_behaving > _probation) {
                        Log.Trace("PhysicalDisk Queue Length behaving well since {0} cycles.",
                            _behaving);
                    } else if (_behaving < 0) {
                        Log.Info("PhysicalDisk Queue Length: integer wrap around happened, " +
                                 "resetting probation counter (no reason to worry).");
                        _behaving = _probation + 1;
                    }
                }
            }
            catch (Exception ex) {
                Log.Error("UpdatePhysicalDiskLoad failed: {0}", ex.Message);
            }
            finally {
                _monitoringTimer.Enabled = true;
            }
            Log.Log(LogPerformanceReadings, "PhysicalDisk Queue Length: {0:0.000} {1}",
                _loadReadings[3], _loadReadings[3] < Limit ? " [" + _behaving + "]" : "");
        }

        /// <summary>
        /// Raise the "LoadAboveLimit" event.
        /// </summary>
        protected virtual void OnLoadAboveLimit() {
            HighLoad = true;
            LoadAboveLimit?.Invoke(this, EventArgs.Empty);
        }

        /// <summary>
        /// Raise the "LoadBelowLimit" event.
        /// </summary>
        protected virtual void OnLoadBelowLimit() {
            HighLoad = false;
            LoadBelowLimit?.Invoke(this, EventArgs.Empty);
        }
    }
}