Sophis Risque: how to limit memory allocation on Citrix/Windows 7?
Jul
8
Written by:
7/8/2014 11:07 AM
In one of my previous posts that can be found there: http://www.it-quants.com/Blogs/tabid/83/EntryId/39/Sophis-Risque-how-to-manage-limit-memory-allocation.aspx, I've detailed a method that works fine on the following OS: Windows XP and Windows 8, with or without Citrix, using the MS CreateJobObject method.
Unfortunately, Microsoft has changed the behaviour of the jobs on Windows 7, and limit the creation to one job on this OS. Citrix is using already one job to control the limitations that can be driven through their configuration manager. For example, we will consider the case where the memory consumption is configured per Sophis user. The goal of this post is to explain the solution that could be retained, without exporting the rights to the Citrix configuration and let keeping Risque/Value manage the memory consumption as before.
In a nutshell, the only solution is to create a thread that will check the memory by polling it every x seconds. The memory configuration for each user is stored in database. The implementation of the memory check will:
- load the configuration for the user
- start the thread and giving the limit to check at the same time
- use the psapi callbacks in order to detect f the Peak Working Set Memory (pwss) is exceeded or not
- when the pwss limit is reached, give the possiblity (timeout) to quit properly the application,
- after xx seconds of timeout, kill the process by calling MS SDK method TerminateProcess
- otherwise, when Sophis quits, stop the memory check thread.
Finally, the code for the thread should look like this one:
Header:
#pragma once
class
CMemoryCheckThread
{
public
:
static
CMemoryCheckThread& getInstance();
bool
Start(
double
memoryLimit);
void
Stop();
protected
:
HANDLE
m_hEvent;
HANDLE
m_hThread;
long
m_timeout;
long
m_timeoutOnExit;
double
m_memoryLimit;
static
DWORD
CheckMemory(
void
*param);
static
DWORD
KillProcess(
void
*param);
private
:
CMemoryCheckThread();
static
CMemoryCheckThread g_instance;
};
.cpp:
#include "stdafx.h"
#include "MemoryCheckThread.h"
CMemoryCheckThread CMemoryCheckThread::g_instance;
CMemoryCheckThread& CMemoryCheckThread::getInstance()
{
return
g_instance;
}
CMemoryCheckThread::CMemoryCheckThread()
: m_hThread(NULL),
m_hEvent(NULL),
m_timeout(30),
m_timeoutOnExit(300),
m_memoryLimit(0.)
{
}
bool
CMemoryCheckThread::Start(
double
memoryLimit)
{
bool
_result =
false
;
if
(m_hThread==NULL)
{
m_timeout = 30;
//30s
m_timeoutOnExit = 300;
//5'
try
{
sophis::misc::ConfigurationFileWrapper::getEntryValue(
"PERF"
,
"timeout"
,m_timeout);
}
catch
(...)
{
}
try
{
sophis::misc::ConfigurationFileWrapper::getEntryValue(
"PERF"
,
"timeoutOnExit"
,m_timeoutOnExit);
}
catch
(...)
{
}
m_hEvent = ::CreateEvent(NULL,FALSE,FALSE,NULL);
DWORD
dwThreadID = 0;
if
(m_hEvent!=NULL)
{
m_memoryLimit = memoryLimit;
m_hThread = CreateThread(NULL,
// default security
0,
// default stack size
CheckMemory,
// name of the thread function
this
,
// the object in parameter
0,
// default startup flags
&dwThreadID);
_result = m_hThread!=NULL;
}
}
return
_result;
}
void
CMemoryCheckThread::Stop()
{
if
(m_hEvent!=NULL)
::SetEvent(m_hEvent);
if
(m_hThread!=NULL)
{
WaitForSingleObject(m_hThread,30000);
::CloseHandle(m_hThread);
}
if
(m_hEvent!=NULL)
CloseHandle(m_hEvent);
}
DWORD
CMemoryCheckThread::KillProcess(
void
* param)
{
CMemoryCheckThread* _instance = (CMemoryCheckThread*)param;
if
(_instance!=NULL)
{
DWORD
dwRet = ::WaitForSingleObject(_instance->m_hEvent, _instance->m_timeoutOnExit * 1000);
if
(dwRet!=WAIT_OBJECT_0)
{
TerminateProcess(GetCurrentProcess(),-1);
}
}
return
0;
}
DWORD
CMemoryCheckThread::CheckMemory(
void
*param)
{
CMemoryCheckThread* _instance = (CMemoryCheckThread*)param;
while
(
true
&& _instance)
{
DWORD
dwRet = ::WaitForSingleObject(_instance->m_hEvent, _instance->m_timeout * 1000);
if
(dwRet==WAIT_TIMEOUT)
{
HANDLE
hProcess = GetCurrentProcess();
PROCESS_MEMORY_COUNTERS pmc;
if
(hProcess)
{
if
(GetProcessMemoryInfo( hProcess, &pmc,
sizeof
( pmc ) ) )
{
double
pwss = ((
float
)pmc.PeakWorkingSetSize) / (
double
)(1024.0 * 1024.0 * 1024) ;
if
(pwss>_instance->m_memoryLimit)
{
DWORD
dwThreadID;
// need to create a thread since MessageBox will keep the hand after until the user release it
HANDLE
hThread = CreateThread(NULL,
// default security
0,
// default stack size
KillProcess,
// name of the thread function
_instance,
// the object in parameter
0,
// default startup flags
&dwThreadID);
if
(hThread!=NULL)
{
char
_buffer[256];
_snprintf(_buffer,
sizeof
(_buffer),
"Memory limit (%.2f GB>%.2f GB) reached, please kill your Sophis session!\nIf nothing is done, it will be killed in %i mn.\nContact the support if you need more memory."
,pwss,_instance->m_memoryLimit,_instance->m_timeoutOnExit/60); MessageBox(NULL,_buffer,
"Sophis Risque"
,MB_SERVICE_NOTIFICATION|MB_ICONERROR);
WaitForSingleObject(hThread,INFINITE);
CloseHandle(hThread);
}
else
TerminateProcess(hProcess,-1);
}
}
}
}
else
// exit the thread when no timeout
break
;
}
return
0;
}
In the Sophis UNIVERSAL_MAIN, just a call to
CMemoryCheckThread::getInstance().Start(4.0); // to limit to 4.0 Gb
And in the DllMain entry point, on the DLL_PROCESS_DETACH, the thread has to be stopped by calling:
CMemoryCheckThread::getInstance().Stop();
On Windows 8, CreateJobObject can create several jobs per process as on XP, the limitation encountered on Windows 7 was removed by Microsoft.