Search code examples
c#.netwindbgmemory-profilingmemory-dump

How to calculate Retained size (in bytes) of Dictionary<TKey,TValue> in memory dump, using WinDbg?


I have an inmemory cache (.NET 6), like this:

struct CacheItem
{
  private byte[] content;
  private long lastUsage;
  /// other fields
}

var cache = new ConcurrentDictionary<string, CacheItem>();

I would like to find out, how much memory do I spend on it. I can get rough estimation, using GC Gen 2 heap size, but this heap contains not only my cache.

Also, I may try to calculate inmemory cache size from an application, adding a string size, a content size, a struct size, but it will give really rough estimation, because I don't know, how fields are aligned, and I don't know object headers, and so on.

So, I can take a full memory dump. I know, that it is possible to get an object size, using WinDbg, but in my case I want to get a Retained size (meaning, with sizes of all data inside it) of the whole Dictionary. So, is it possible to do, using WinDbg, or any other tool?


Solution

  • WinDbg is not particularly good for analyzing .NET memory issues. Yes, there is the SOS extension and yes, there are heap related commands. But it's simply tedious work and time consuming. You could try !objsize, though.

    0:000> .loadby sos coreclr
    
    0:000> !dumpheap -stat -type ConcurrentDictionary
    Statistics:
              MT Count TotalSize Class Name
    7ffd918d1d50     1        48 System.Collections.Concurrent.ConcurrentDictionary<System.String, CacheItem>
    [...]
    Total 2.595 objects, 216.592 bytes
    
    0:000> !dumpheap -mt 7ffd918d1d50 
             Address               MT           Size
        01881325abc0     7ffd918d1d50             48 
    
    Statistics:
              MT Count TotalSize Class Name
    7ffd918d1d50     1        48 System.Collections.Concurrent.ConcurrentDictionary<System.String, CacheItem>
    Total 1 objects, 48 bytes
    
    
    0:000> !objsize 01881325abc0  
    [...]
    Statistics:
    [...]
    Total 4.030 objects, 196.560 bytes
    

    I recommend using a tool which was especially made for this purpose, like JetBrains dotMemory. That's not a general recommendation, but I happen to own a license, so I can tell you a little bit about it.

    1. Make sure you have a good .NET memory dump
    2. Start JetBrains dotMemory and choose "Import Process Dump..."
    3. Click on "Snaphot #1" (which is the only one there is)
    4. Click on "Types"
    5. Enter "ConcurrentDictionary" into the filter. Let's hope there aren't too many.
    6. Click on "ConcurrentDictionary<String, CacheItem>" or whatever matches your cache.
    7. Have a look at the column "retained bytes"

    Screenshot with retained bytes

    Code used:

    using System.Collections.Concurrent;
    
    Console.WriteLine("Hello, World!");
    
    
    
    var cache = new ConcurrentDictionary<string, CacheItem>();
    
    for (int i = 0; i < 1000; i++)
    {
        cache.TryAdd("a"+i, new CacheItem() { content = "aaaaa"+i, lastUsage = 12+i });
    }
    
    throw new Exception("fail");
    
    struct CacheItem {
        public string content;
        public long lastUsage;
        /// other fields
    }