Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Set Process Memory Limit with Process Governor

4.86/5 (4 votes)
21 Nov 2013CPOL3 min read 34.7K  
I wrote this tool to test my .NET applications (including web applications) for memory leaks.

I would like to introduce you to Process Governor – a new tool I added to my .NET diagnostics toolkit. This application allows you to set a limit on a memory committed by a process. In Windows, committed memory is actually all private memory that the process uses. I wrote this tool to test my .NET applications (including web applications) for memory leaks. With it, I can check if under heavy load, they won’t throw OutOfMemoryException.

Usage Scenarios

The command line syntax is really simple, you just specify a memory limit using the -maxmem switch and then provide your application path and its arguments. You can either start a new process:

procgov.exe -maxmem 30M .\example\TestMemory.exe

or attach to an already running one:

procgov  -maxmem 30M -p 6040

The -p parameter corresponds to the process identifier (PID). The difference between those two is that when you start a process, it has the memory constraint already set – so we will receive an OutOfMemory at the moment it occurs. When you attach to a process that already exceeded the memory limit, it will be immediately terminated so no OutOfMemory will be thrown.

How It Works?

Process Governor uses a system job object to apply constraints to a process. Unfortunately, there is no managed API to work with jobs so I needed to import some functions from pinvoke.net. Let’s quickly analyze the source code of this application. Before we can assign a process to a job (and apply constraints on it), we need to obtain its handle. When attaching to a process, we will use the OpenProcess function:

C#
hProcess = CheckResult(ApiMethods.OpenProcess(ProcessAccessFlags.AllAccess, false, pid));

If we start a new process, we will use the CreateProcess function:

C#
PROCESS_INFORMATION pi;
STARTUPINFO si = new STARTUPINFO();

CheckResult(ApiMethods.CreateProcess(null, String.Join(" ", procargs), 
           IntPtr.Zero, IntPtr.Zero, false,
           CreateProcessFlags.CREATE_SUSPENDED | CreateProcessFlags.CREATE_NEW_CONSOLE,
           IntPtr.Zero, null, ref si, out pi));

I haven’t used the Process.Create managed method as it does not allow to start a process in a suspended state.

After obtaining a process handle, we can start working with a job object. First, we need to create it:

C#
var securityAttributes = new SECURITY_ATTRIBUTES();
securityAttributes.nLength = Marshal.SizeOf(securityAttributes);

hJob = CheckResult(ApiMethods.CreateJobObject
(ref securityAttributes, "procgov-" + Guid.NewGuid()));

Then, we will create a completion port for listening to job events:

C#
// create completion port
hIOCP = CheckResult(ApiMethods.CreateIoCompletionPort(ApiMethods.INVALID_HANDLE_VALUE, 
        IntPtr.Zero, IntPtr.Zero, 1));
var assocInfo = new JOBOBJECT_ASSOCIATE_COMPLETION_PORT {
    CompletionKey = IntPtr.Zero,
    CompletionPort = hIOCP
};
uint size = (uint)Marshal.SizeOf(assocInfo);
CheckResult(ApiMethods.SetInformationJobObject
        (hJob, JOBOBJECTINFOCLASS.AssociateCompletionPortInformation,
        ref assocInfo, size));

// start listening thread
listener = new Thread(CompletionPortListener);
listener.Start(hIOCP);

The CompletionPortListener method has a simple switch and transforms message identifier to some meaningful messages. That’s a pity that there is no managed API for I/O completion port. They seem to be one of the greatest mechanism in Windows to asynchronously communicate between threads. The name is a bit misleading as it suggests that there may be used only for I/O operations – the truth is that you may use them to any type of asynchronous communication between threads (the job notifier is one of examples). In our case, as we have only one process run in a job, so one listening thread is enough.

Finally, we apply a process memory limit (if provided) and assign the newly created process to our job:

C#
if (maxmem > 0.0f) {
    // configure constraints
    var limitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
        BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION {
            LimitFlags = JobInformationLimitFlags.JOB_OBJECT_LIMIT_PROCESS_MEMORY
                        | JobInformationLimitFlags.JOB_OBJECT_LIMIT_BREAKAWAY_OK
                        | JobInformationLimitFlags.JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
        },
        ProcessMemoryLimit = (UIntPtr)maxmem
    };
    size = (uint)Marshal.SizeOf(limitInfo);
    CheckResult(ApiMethods.SetInformationJobObject(hJob, JOBOBJECTINFOCLASS.ExtendedLimitInformation,
            ref limitInfo, size));
}

// assign a process to a job to apply constraints
CheckResult(ApiMethods.AssignProcessToJobObject(hJob, hProcess));

Remember that we created a process in a suspended mode so if the PID was not provided, we need to resume the main process thread:

C#
// resume process main thread (if it was started by us)
if (pid == 0) {
    CheckResult(ApiMethods.ResumeThread(pi.hThread));
    // and we can close the thread handle
    CloseHandle(pi.hThread);
}

The process should be now running and we can wait for its completion:

C#
if (ApiMethods.WaitForSingleObject(hProcess, ApiMethods.INFINITE) == 0xFFFFFFFF) {
    throw new Win32Exception();
}

The source code and the application can be downloaded from the .NET diagnostics toolkit codeplex site. I encourage you to have a deeper look at what the job object provides. In my application, I’m setting limit on a process committed memory, but you may as well restrict process CPU usage and its working set size. Have fun with jobs and fight with memory leaks. :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)