File: System\Runtime\Caching\Counters.cs
Web Access
Project: src\runtime\src\libraries\System.Runtime.Caching\src\System.Runtime.Caching.csproj (System.Runtime.Caching)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;

namespace System.Runtime.Caching
{
#if NET
    [UnsupportedOSPlatform("browser")]
#endif
    internal sealed class Counters : EventSource
    {
#if NET
        private const string EVENT_SOURCE_NAME_ROOT = "System.Runtime.Caching.";
        private const int NUM_COUNTERS = 7;

        private DiagnosticCounter[] _counters;

        // Backing storage for the raw counter values.
        //
        // These are updated with Interlocked ops on every cache Get/Add/Remove, so the layout matters:
        //
        // 1. Named fields instead of a long[]. Indexing an array forces the JIT to emit a bounds check,
        //    which loads the array's length field. That length lives in the same cache line as the
        //    elements of a small array, so every increment performed a plain load of a line that is
        //    simultaneously the target of a contended atomic RMW. That defeats the "far atomic" handling
        //    LSE-capable hardware uses to keep contended counters resident at the shared cache, and turns
        //    each increment into a cache-line migration. Struct fields have no length to load.
        //
        // 2. Counters are grouped onto cache lines by how MemoryCacheStore actually updates them, so that
        //    unrelated operations running concurrently do not falsely share a line. Counters that are
        //    always bumped by the *same* operation stay together, because splitting those would force one
        //    operation to acquire several contended lines with fully-ordered atomics back to back:
        //      - Entries + Turnover: always bumped together by Add()/RemoveFromCache()
        //      - Hits:               bumped by a Get() that hits
        //      - Misses:             bumped by a Get() that misses
        //      - Trims:              bumped in batches by the (rare) trim path
        private CounterValues _counterValues;

        private const int CacheLineSize = Internal.PaddingHelpers.CACHE_LINE_SIZE;

        [StructLayout(LayoutKind.Explicit, Size = CacheLineSize * 4)]
        private struct CounterValues
        {
            [FieldOffset(CacheLineSize * 0)] public long Entries;
            [FieldOffset(CacheLineSize * 0 + 8)] public long Turnover;

            [FieldOffset(CacheLineSize * 1)] public long Hits;

            [FieldOffset(CacheLineSize * 2)] public long Misses;

            [FieldOffset(CacheLineSize * 3)] public long Trims;
        }

        internal Counters(string cacheName) : base(EVENT_SOURCE_NAME_ROOT + (cacheName ?? throw new ArgumentNullException(nameof(cacheName))))
        {
            InitDisposableMembers();
        }

        private void InitDisposableMembers()
        {
            bool dispose = true;

            try
            {
                _counters = new DiagnosticCounter[NUM_COUNTERS];
                _counters[(int)CounterName.Entries] = CreatePollingCounter("entries", "Cache Entries", () => Interlocked.Read(ref _counterValues.Entries));
                _counters[(int)CounterName.Hits] = CreatePollingCounter("hits", "Cache Hits", () => Interlocked.Read(ref _counterValues.Hits));
                _counters[(int)CounterName.Misses] = CreatePollingCounter("misses", "Cache Misses", () => Interlocked.Read(ref _counterValues.Misses));
                _counters[(int)CounterName.Trims] = CreatePollingCounter("trims", "Cache Trims", () => Interlocked.Read(ref _counterValues.Trims));

                _counters[(int)CounterName.Turnover] = new IncrementingPollingCounter("turnover", this,
                    () => Interlocked.Read(ref _counterValues.Turnover))
                {
                    DisplayName = "Cache Turnover Rate",
                };

                // This two-step dance with hit-ratio was an old perf-counter artifact: the ratio used to be
                // tracked as a pair of raw counters (HitRatio, incremented on every hit, and HitRatioBase,
                // incremented on every hit and every miss). Neither raw value is observable - only the
                // percentage computed below is - and they are exactly redundant with Hits and Hits + Misses.
                // Deriving the ratio lets the Get() hot path do a single Interlocked op instead of three.
                // 0 hits and 0 misses still yields NaN, as it did with the raw counters. Hits is read once
                // and reused for both the numerator and the denominator, so unlike the separate
                // HitRatio/HitRatioBase reads the result can never transiently exceed 100%.
                _counters[(int)CounterName.HitRatio] = new PollingCounter("hit-ratio", this,
                    () =>
                    {
                        double hits = Interlocked.Read(ref _counterValues.Hits);
                        return (hits / (hits + Interlocked.Read(ref _counterValues.Misses))) * 100d;
                    })
                {
                    DisplayName = "Cache Hit Ratio",
                };
                //_counters[(int)CounterName.HitRatioBase] = n/a;

                dispose = false;
            }
            finally
            {
                if (dispose)
                    Dispose();
            }
        }

        private PollingCounter CreatePollingCounter(string name, string displayName, Func<double> getValue)
        {
            return new PollingCounter(name, this, getValue)
            {
                DisplayName = displayName,
            };
        }

        public new void Dispose()
        {
            DiagnosticCounter[] counters = _counters;

            // ensure this only happens once
            if (counters != null && Interlocked.CompareExchange(ref _counters, null, counters) == counters)
            {
                for (int i = 0; i < NUM_COUNTERS; i++)
                {
                    counters[i]?.Dispose();
                }
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private ref long GetCounterRef(CounterName name)
        {
            switch (name)
            {
                case CounterName.Entries: return ref _counterValues.Entries;
                case CounterName.Hits: return ref _counterValues.Hits;
                case CounterName.Misses: return ref _counterValues.Misses;
                case CounterName.Trims: return ref _counterValues.Trims;
                case CounterName.Turnover: return ref _counterValues.Turnover;
                default: throw new UnreachableException();
            }
        }

        internal void Increment(CounterName name) => Interlocked.Increment(ref GetCounterRef(name));

        internal void IncrementBy(CounterName name, long value) => Interlocked.Add(ref GetCounterRef(name), value);

        internal void Decrement(CounterName name) => Interlocked.Decrement(ref GetCounterRef(name));
#else
#pragma warning disable CA1822, IDE0060
        internal Counters(string cacheName)
        {
        }
        public new void Dispose()
        {
        }
        internal void Increment(CounterName name)
        {
        }
        internal void IncrementBy(CounterName name, long value)
        {
        }
        internal void Decrement(CounterName name)
        {
        }
#pragma warning restore CA1822, IDE0060
#endif
    }
}