32

Currently we're using the .NET Memory Cache 4.0 for the Caching requirements. (not ASP.NET Cache, not any external Cache)

Looking at the '.NET Memory Cache 4.0' performance counters, there is data around Cache Hits, Misses, Entries, Trims etc. but nothing related to Size.

Is there is a way of measuring/knowing the current size of the Cache used by the Production Application?

I want to be able to capture this data at various points in time and get the average size of the cache.

Raja Nadar
  • 9,409
  • 2
  • 32
  • 41
  • That can be somehow related http://stackoverflow.com/questions/1128315/find-size-of-object-instance-in-bytes-in-c-sharp/1128674#1128674 – nuclear sweet Mar 13 '14 at 23:18
  • thanks for the link 'nuclear sweet'. but i am looking for a perf counter/external commandlet (app fabric stats) sort of way of getting this data. not using inline code to measure the memory myself. – Raja Nadar Mar 14 '14 at 09:19
  • also, so far, i found that the 'private working set 64' of the process has given an estimate of the memory usage. it is not the most accurate of numbers, but is the best i can get right now. i am looking for a more reliable & accurate pointer. – Raja Nadar Mar 14 '14 at 09:21

7 Answers7

30

It is an ugly implementation detail that Microsoft did not want to expose at all. Measuring object sizes in .NET is not in general possible. MemoryCache uses a fairly nasty backdoor to implement its memory limit trigger, it uses the DACCESS component of the CLR, actually intended to help implement memory profilers.

You can see it with the debugger so it isn't like you can't get to it. You just have to write very ugly code to dig through the private fields:

using System;
using System.Reflection;
using System.Runtime.Caching;

public static class MemoryCacheHackExtensions {
    public static long GetApproximateSize(this MemoryCache cache) {
        var statsField = typeof(MemoryCache).GetField("_stats", BindingFlags.NonPublic | BindingFlags.Instance);
        var statsValue = statsField.GetValue(cache);
        var monitorField = statsValue.GetType().GetField("_cacheMemoryMonitor", BindingFlags.NonPublic | BindingFlags.Instance);
        var monitorValue = monitorField.GetValue(statsValue);
        var sizeField = monitorValue.GetType().GetField("_sizedRef", BindingFlags.NonPublic | BindingFlags.Instance);
        var sizeValue = sizeField.GetValue(monitorValue);
        var approxProp = sizeValue.GetType().GetProperty("ApproximateSize", BindingFlags.NonPublic | BindingFlags.Instance);
        return (long)approxProp.GetValue(sizeValue, null);
    }
}

Seemed to work pretty well on .NET 4.6.1, not extensively tested. This is okay to get the lay of the land, just don't depend on it since it may break with any .NET update.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • thanks for the answer @hans but as i mentioned in the question, i am looking for an external tool to measure this (not instrumented code) kind of the appfabrics-stats commandlets, even if i have to run them on the machine. i guess, being a memory cache, and the fact that perf counters are not exposed, going the code way maybe the only way to measure the size. our code is a busy production code, and i really don't want to poke it with reflection/quick-fix code, since multiple layers in our app domain use various named caches and do a lot of read/write. good to know about this approach though – Raja Nadar Feb 25 '16 at 02:59
  • That's just not possible, MemoryCache doesn't make that measurement externally visible. You can only do it in-process. – Hans Passant Feb 25 '16 at 06:27
  • My sizeref is alway zero. I can see the values in cache in debug mode but this code always return zero – Zeus Dec 19 '17 at 20:12
  • 1
    On .net 4.7.2 the code broke had to change "_sizedRef" to "_sizedRefMultiple" but I was getting size zero – gpanagakis Jul 14 '20 at 08:55
  • gpanagakis fix changing `_sizedRef` to `_sizedRefMultiple` worked for me. When you get zero, wait two minutes, since that seems to be the polling interval at which the statistics are getting updated. – Oliver Schimmer Oct 28 '20 at 13:43
10

I took the original code and had to make a minor adjustment, I used "_sizedRefMultiple" instead of "_sizedRef" to make it work with .NET 4.6.

public static class MemoryCacheHackExtensions
{
    public static long GetApproximateSize(this MemoryCache cache)
    {
        var statsField = typeof(MemoryCache).GetField("_stats", BindingFlags.NonPublic | BindingFlags.Instance);
        var statsValue = statsField.GetValue(cache);
        var monitorField = statsValue.GetType().GetField("_cacheMemoryMonitor", BindingFlags.NonPublic | BindingFlags.Instance);
        var monitorValue = monitorField.GetValue(statsValue);
        var sizeField = monitorValue.GetType().GetField("_sizedRefMultiple", BindingFlags.NonPublic | BindingFlags.Instance);
        var sizeValue = sizeField.GetValue(monitorValue);
        var approxProp = sizeValue.GetType().GetProperty("ApproximateSize", BindingFlags.NonPublic | BindingFlags.Instance);
        return (long)approxProp.GetValue(sizeValue, null);
    }
}
Shahal Hazan
  • 371
  • 1
  • 3
  • 7
  • My sizerefmultiple is alway zero. I can see the values in cache in debug mode but this code always return zero – Zeus Dec 19 '17 at 20:13
  • 2
    had to use the same trick on .NET 4.5 as well ' var sizeVarName = "_sizedRef"; #if NET45 sizeVarName = "_sizedRefMultiple"; #endif ' – hal9000 Apr 25 '18 at 17:04
5

Finally, in .NET 7 preview, you can access the measure very easily. Some new metrics were added to IMemoryCache.

  • MemoryCacheStatistics which holds cache hit/miss/estimated size and count for IMemoryCache.
  • GetCurrentStatistics returns an instance of MemoryCacheStatistics, or null when TrackStatistics flag is not enabled. The library has a built-in implementation available for MemoryCache.

Read more here.

Misha Zaslavsky
  • 8,414
  • 11
  • 70
  • 116
  • Unfortunately, this isn't as useful as one might expect. I spent some time migrating to Microsoft.Extensions.Caching.MemoryCache, only to find this gem from the documentation https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-7.0#use-setsize-size-and-sizelimit-to-limit-cache-size - basically, theses stats mean nothing because the new memcache expects the developer to provide sizes for everything, and effectively sizing memory usage for objects in .net at runtime is a long-discussed, dead-end endeavour ): – daf Jul 03 '23 at 09:13
2

As an alternative, you could also implement the IMemoryCacheManager interface and assign that to the global ObjectCache.Host property. That requires that you are allowed to do this, i.e. no other component in your application has already done so (ASP.NET comes to mind, but I'm not sure). Personally, I use that approach in a Console/Windows Service application with no issues.

Note also, that you will only get cache sizes after a full GC or so, but that shouldn't be any different with Hans' approach.

Also note, that the below code works for named MemoryCaches, i.e. not on the instance itself.

Quite a view "but"s. But then, it doesn't require reflection.

So, here is the code.

public static class MemoryCacheHelper
{
    private static readonly MemoryCacheServiceProvider s_serviceProvider = new MemoryCacheServiceProvider();

    static MemoryCacheHelper()
    {
        try
        {
            ObjectCache.Host = s_serviceProvider;
        }
        catch (InvalidOperationException ex)
        {
            // ObjectCache.Host can only be set once.
        }
    }

    public static MemoryCache Create(string name, NameValueCollection config) 
    {
        return new MemoryCache(name, config);
    }

    // Return approximate cache size and when that value was last determined.
    public static Tuple<long, DateTime> GetApproximateSize(string name)
    {
        return s_serviceProvider.GetApproximateSize(cache.Name);
    }

    private class MemoryCacheServiceProvider : IMemoryCacheManager, IServiceProvider
    {
        private readonly object m_lock = new object();
        private readonly IDictionary<string, Tuple<long, DateTime>> m_sizes = new Dictionary<string, Tuple<long, DateTime>>();

        public Tuple<long, DateTime> GetApproximateSize(string name)
        {
            lock (m_lock)
            {
                Tuple<long, DateTime> info;
                if (m_sizes.TryGetValue(name, out info))
                    return info;
                return null;
            }
        }

        void IMemoryCacheManager.UpdateCacheSize(long size, MemoryCache cache)
        {
            lock (m_lock)
            {
                // The UpdateCacheSize() method will be called based on the configured "pollingInterval"
                // for the respective cache. That value defaults to 2 minutes. So this statement doesn't
                // fire to often and as a positive side effect we get some sort of "size-heartbeat" which
                // might help when troubleshooting.
                m_sizes[cache.Name] = Tuple.Create(size, DateTime.UtcNow);
            }
        }

        void IMemoryCacheManager.ReleaseCache(MemoryCache cache)
        {
            lock (m_lock)
            {
                m_sizes.Remove(cache.Name);
            }
        }

        object IServiceProvider.GetService(Type serviceType)
        {
            if (serviceType == typeof(IMemoryCacheManager))
            {
                return this;
            }

            return null;
        }
    }
Community
  • 1
  • 1
Christian.K
  • 47,778
  • 10
  • 99
  • 143
  • thanks for the answer @christian.K but as i mentioned in the question, i am looking for an external tool to measure this (not instrumented code) kind of the appfabrics-stats commandlets, even if i have to run them on the machine. i guess, being a memory cache, and the fact that perf counters are not exposed, going the code way maybe the only way to measure the size. our code is a busy production code, and i really don't want to poke it with single host, since multiple layers in our app domain use various named caches. but good to know these code level options – Raja Nadar Feb 25 '16 at 02:58
  • As @HansPassant said there is not really an external tool. One option that is possilbe, albeit rather involved, would be to use something like `Microsoft.Runtime.Diagnostics` (basically a managed version of WinDbg) and try to read the internals (as seen in Hans' answer) during runtime (and/or from an external tool that you code using this package). Of course, you could also run WinDbg and a script in batch-mode to achieve the same. I'm not sure if that is something you should do in production though (except for eventual troubleshooting), so I'm not going to provide an answer for that. – Christian.K Mar 05 '16 at 11:16
  • This is exactly what I was looking for! A note: When I tried the above code in LINQPad, at first it didn't seem to work. That's because "UpdateCacheSize" is called by the cache [on an interval](https://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache.pollinginterval.aspx), which defaults to 2 minutes. If you want to see it update more frequently (or you're impatient), you have to change that interval via `app.config` or via a constructor argument, e.g.: `new MemoryCache("foo", new NameValueCollection { { "PollingInterval", "00:00:01" } })`. – Allon Guralnek Dec 21 '16 at 08:49
  • And of course you have to call `GC.Collect()` to force a full GC in a test application to see `UpdateCacheSize` being called, otherwise it may never be. Never call `GC.Collect()` in a real application, though. – Allon Guralnek Dec 21 '16 at 08:59
0

I have written an extension method which iterates through the public properties of an the object and sums up the size values. If it's a IEnumerable or an Array it will iterate through items and calculate their size as well. This is not the best way but works good enough for me since I don't have more complicated requirements than this for my cache.

Note: there seems to be around 600 bytes of overhead per cache entry you should add it to calculations.

    public static class ObjectMemorySizeCalculator
    {
        static int OBJECT_SIZE = IntPtr.Size == 8 ? 24 : 12;
        static int POINTER_SIZE = IntPtr.Size;
        public static long GetMemorySize(this object obj)
        {
            long memorySize = 0;
            Type objType = obj.GetType();

            if (objType.IsValueType)
            {
                memorySize = Marshal.SizeOf(obj);
            }
            else if (objType.Equals(typeof(string)))
            {
                var str = (string)obj;
                memorySize = str.Length * 2 + 6 + OBJECT_SIZE;
            }
            else if (objType.IsArray)
            {
                var arr = (Array)obj;
                var elementType = objType.GetElementType();
                if (elementType.IsValueType)
                {
                    long elementSize = Marshal.SizeOf(elementType);
                    long elementCount = arr.LongLength;
                    memorySize += elementSize * elementCount;
                }                    
                else
                {
                    foreach (var element in arr)
                    {
                        memorySize += element != null ? element.GetMemorySize() + POINTER_SIZE : POINTER_SIZE;
                    }
                }

                memorySize += OBJECT_SIZE + 40;
            }
            else if (obj is IEnumerable)
            {
                var enumerable = (IEnumerable)obj;
                foreach(var item in enumerable)
                {
                    var itemType = item.GetType();
                    memorySize += item != null ? item.GetMemorySize() : 0;
                    if (itemType.IsClass)
                        memorySize += POINTER_SIZE;
                }
                memorySize += OBJECT_SIZE;
            }
            else if (objType.IsClass)
            {
                var properties = objType.GetProperties();
                foreach (var property in properties)
                {
                    var valueObject = property.GetValue(obj);
                    memorySize += valueObject != null ? valueObject.GetMemorySize() : 0;
                    if (property.GetType().IsClass)
                        memorySize += POINTER_SIZE;
                }
                memorySize += OBJECT_SIZE;
            }                

            return memorySize;
        }
brainoverflow98
  • 1,138
  • 11
  • 28
0

I was inspired by Hans Passant's answer when trying to get the size of a MemoryDistributedCache. Here's how the code looks:

public static class MemoryDistributedCacheHackExtensions
{
    public static long GetCacheSize(this MemoryDistributedCache cache)
    {
        var memCacheField = typeof(MemoryDistributedCache).GetField("_memCache", BindingFlags.NonPublic | BindingFlags.Instance);
        var memCacheValue = memCacheField.GetValue(cache);
        var cacheSizeField = memCacheValue.GetType().GetField("_cacheSize", BindingFlags.NonPublic | BindingFlags.Instance);
        var cacheSizeValue = cacheSizeField.GetValue(memCacheValue);
        return (long)cacheSizeValue;
    }
}

So, this is not an answer to the original question. I wanted to put the code here in case someone else runs into the same problem I did.

Amos Long
  • 845
  • 11
  • 14
0

Since .NET 4.7 you can use GetLastSize()

https://learn.microsoft.com/en-us/dotnet/api/system.runtime.caching.memorycache.getlastsize?view=dotnet-plat-ext-6.0

MichaelD
  • 8,377
  • 10
  • 42
  • 47