Memory Related Metrics in C#
Recently we use Grafana to monitor ASP.NET Core apps. We have an interesting observation that sometimes the "allocated memory" is larger than "working set". After investigation, we found the root cause is that the app uses native dlls which operates on unmanaged memory which is not managed by GC. Therefore, how to correctly collect memory related metrics in C#?
We will explain these related concepts in this post.
Basic Concepts
Managed vs Unmanaged Code
To put it very simply, managed code is just that: code whose execution is managed by a runtime.
# Interoperating with unmanaged code
Code that executes under the control of the runtime is called managed code. Conversely, code that runs outside the runtime is called unmanaged code. COM components, ActiveX interfaces, and Windows API functions are examples of unmanaged code.
Unsafe Code
In the common language runtime (CLR), unsafe code is referred to as unverifiable code.
Please note that unmanaged != unsafe. According to their definitions, unsafe code still runs on top of CLR, while unmanaged code is not managed by CLR. In short, unmanaged code must be unsafe, but unsafe code is still "under manage".
GC.GetTotalMemory
# What is private bytes, virtual bytes, working set?
# GC.GetTotalMemory(Boolean) Method
Retrieves the number of bytes currently thought to be allocated. A parameter indicates whether this method can wait a short interval before returning, to allow the system to collect garbage and finalize objects.
WorkingSet
# Process.MaxWorkingSet Property
The working set of a process is the set of memory pages currently visible to the process in physical RAM memory. These pages are resident and available for an application to use without triggering a page fault.
Private Bytes
# Process.PrivateMemorySize64 Property
The value returned by this property represents the current size of memory used by the process, in bytes, that cannot be shared with other processes.
Create Unmanaged Objects in C
If we want to test whether a memory API collects unmanaged memory as
well, we need to create unmanaged object first. Basically, you can write
a native c/c++ dll to directly work with memory. Fortunately, we have an
easier way to create unmanaged objects: Marshal Class
.
Provides a collection of methods for allocating unmanaged memory, copying unmanaged memory blocks, and converting managed to unmanaged types, as well as other miscellaneous methods used when interacting with unmanaged code.
We will use Marshal.AllocHGlobal Method
to request
unmanaged memory.
// Claim 10MB memory from unmanaged memory of the process
IntPtr unmanagedMem = Marshal.AllocHGlobal(10 * 1024 * 1024);
Demo App
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Initial:");
GetMetrics();
var managedMem = new string('x', 10 * 1024 * 1024);
Thread.Sleep(2000);
Console.WriteLine("Request 10MB memory in managed heap");
GetMetrics();
Thread.Sleep(2000);
IntPtr unmanagedMem = Marshal.AllocHGlobal(10 * 1024 * 1024);
// Intended not to call Marshal.FreeHGlobal(unmanagedMem);
Console.WriteLine("Request 10MB unmanaged memory");
GetMetrics();
}
static void GetMetrics()
{
var process = Process.GetCurrentProcess();
var workingSet = process.WorkingSet64 / 1024 / 1024;
var gcTotalMemory = GC.GetTotalMemory(true) / 1024 / 1024;
var privateBytes = process.PrivateMemorySize64 / 1024 / 1024;
Console.WriteLine($"WorkingSet: {workingSet}MB GCTotalMemory: {gcTotalMemory}MB PrivateBytes: {privateBytes}MB");
Console.WriteLine();
}
}
}
Results:
Initial:
WorkingSet: 14MB GCTotalMemory: 0MB PrivateBytes: 8MB
Request 10MB memory in managed heap
WorkingSet: 34MB GCTotalMemory: 20MB PrivateBytes: 28MB
Request 10MB unmanaged memory
WorkingSet: 34MB GCTotalMemory: 20MB PrivateBytes: 38MB
Easy to find that the statistics is align with VS Diagnostic Tools.
Observations
- PrivateBytes includes unmanaged memory
- WorkingSet doesn't include unmanaged memory
- WorkingSet != GCTotalMemory + PrivateBytes, which means they are not directly related