Skip to content

Commit

Permalink
Improve detection of CPU limits when running inside a Container
Browse files Browse the repository at this point in the history
This focuses on better supporting Docker CLI's parameter `--cpus`, which limits the amount of CPU time available to the container (ex: 1.8 means 180% CPU time, ie on 2 cores 90% for each core, on 4 cores 45% on each core, etc.)

All the runtime components depending on the number of processors available are:
 - ThreadPool
 - GC
 - `Environment.ProcessorCount` via `SystemNative::GetProcessorCount`
 - `SimpleRWLock::m_spinCount`
 - `BaseDomain::m_iNumberOfProcessors` (it's used to determine the GC heap to affinitize to)

All the above components take advantage of `--cpus` via `CGroup::GetCpuLimit` with dotnet#12797, allowing to optimize performance in a container/machine with limited resources. This makes sure the runtime components makes the best use of available resources.

In the case of `Environment.ProcessorCount`, the behavior is such that passing `--cpus=1.5` on a machine with 8 processors will return `1`  as shown in https://github.com/dotnet/coreclr/issues/22302#issuecomment-459092299. This behavior is not consistent with [Windows Job Objects](https://docs.microsoft.com/en-us/windows/desktop/api/winnt/ns-winnt-jobobject_cpu_rate_control_information) which still returns the number of processors for the container/machine even if it only gets parts of the total number of cycles.

This behavior is erroneous because the container still has access to the full range of processors on the machine, and only its _processor time_ is limited. For example, in the case of a 4 processors machine, with a value of `--cpus=1.8`, there can be 4 threads running in parallel even though each thread will only get `1.8 / 8 = .45` or 45% of all cycles of each processor.

The work consist in reverting the behavior of `SystemNative::GetProcessorCount` to pre dotnet#12797.
  • Loading branch information
luhenry committed Mar 27, 2019
1 parent e6c49f7 commit 58443dc
Show file tree
Hide file tree
Showing 12 changed files with 92 additions and 37 deletions.
7 changes: 0 additions & 7 deletions src/classlibnative/bcltype/system.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,13 +343,6 @@ INT32 QCALLTYPE SystemNative::GetProcessorCount()
processorCount = systemInfo.dwNumberOfProcessors;
}

#ifdef FEATURE_PAL
uint32_t cpuLimit;

if (PAL_GetCpuLimit(&cpuLimit) && cpuLimit < (uint32_t)processorCount)
processorCount = cpuLimit;
#endif

END_QCALL;

return processorCount;
Expand Down
3 changes: 3 additions & 0 deletions src/gc/env/gcenv.os.h
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ class GCToOSInterface
// The number of processors
static uint32_t GetCurrentProcessCpuCount();

// Get the budget for the current processor
static double GetCurrentProcessCpuBudget();

// Sets the calling thread's affinity to only run on the processor specified
// in the GCThreadAffinity structure.
// Parameters:
Expand Down
23 changes: 19 additions & 4 deletions src/gc/gc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34212,11 +34212,26 @@ HRESULT GCHeap::Initialize()

#ifdef MULTIPLE_HEAPS
nhp_from_config = static_cast<uint32_t>(GCConfig::GetHeapCount());

// GetCurrentProcessCpuCount only returns up to 64 procs.
uint32_t nhp_from_process = GCToOSInterface::CanEnableGCCPUGroups() ?
GCToOSInterface::GetTotalProcessorCount():
GCToOSInterface::GetCurrentProcessCpuCount();
uint32_t nhp_from_process;
if (GCToOSInterface::CanEnableGCCPUGroups())
{
nhp_from_process = GCToOSInterface::GetTotalProcessorCount();
}
else
{
uint32_t cpuCount = GCToOSInterface::GetCurrentProcessCpuCount();
double cpuBudget = GCToOSInterface::GetCurrentProcessCpuBudget();
assert (1.0 <= cpuBudget && cpuBudget < (double) UINT32_MAX);

if (cpuBudget < cpuCount)
{
cpuCount = (uint32_t)(cpuBudget + 0.5);
}

nhp_from_process = cpuCount;
}

if (nhp_from_config)
{
Expand Down
10 changes: 5 additions & 5 deletions src/gc/unix/cgroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ class CGroup
return result;
}

static bool GetCpuLimit(uint32_t *val)
static bool GetCpuLimit(double *val)
{
long long quota;
long long period;
long long cpu_count;
double cpu_count;

quota = ReadCpuCGroupValue(CFS_QUOTA_FILENAME);
if (quota <= 0)
Expand All @@ -115,11 +115,11 @@ class CGroup
// Cannot have less than 1 CPU
if (quota <= period)
{
*val = 1;
*val = 1.0;
return true;
}

cpu_count = quota / period;
cpu_count = (double) quota / period;
if (cpu_count < UINT32_MAX)
{
*val = cpu_count;
Expand Down Expand Up @@ -512,7 +512,7 @@ bool GetPhysicalMemoryUsed(size_t* val)
return result;
}

bool GetCpuLimit(uint32_t* val)
bool GetCpuLimit(double* val)
{
if (val == nullptr)
return false;
Expand Down
12 changes: 7 additions & 5 deletions src/gc/unix/gcenv.unix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ static pthread_mutex_t g_flushProcessWriteBuffersMutex;

size_t GetRestrictedPhysicalMemoryLimit();
bool GetPhysicalMemoryUsed(size_t* val);
bool GetCpuLimit(uint32_t* val);
bool GetCpuLimit(double *val);

static size_t g_RestrictedPhysicalMemoryLimit = 0;

Expand Down Expand Up @@ -529,7 +529,6 @@ bool GCToOSInterface::GetCurrentProcessAffinityMask(uintptr_t* processAffinityMa
uint32_t GCToOSInterface::GetCurrentProcessCpuCount()
{
uintptr_t pmask, smask;
uint32_t cpuLimit;

if (!GetCurrentProcessAffinityMask(&pmask, &smask))
return 1;
Expand All @@ -553,12 +552,15 @@ uint32_t GCToOSInterface::GetCurrentProcessCpuCount()
if (count == 0 || count > 64)
count = 64;

if (GetCpuLimit(&cpuLimit) && cpuLimit < count)
count = cpuLimit;

return count;
}

double GCToOSInterface::GetCurrentProcessCpuBudget()
{
double cpuLimit;
return GetCpuLimit (&cpuLimit) ? cpuLimit : (double) GetCurrentProcessCpuCount();
}

// Return the size of the user-mode portion of the virtual address space of this process.
// Return:
// non zero if it has succeeded, 0 if it has failed
Expand Down
5 changes: 5 additions & 0 deletions src/gc/windows/gcenv.windows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,11 @@ uint32_t GCToOSInterface::GetCurrentProcessCpuCount()
return count;
}

double GCToOSInterface::GetCurrentProcessCpuBudget()
{
return (double) GetCurrentProcessCpuCount();
}

// Return the size of the user-mode portion of the virtual address space of this process.
// Return:
// non zero if it has succeeded, 0 if it has failed
Expand Down
1 change: 1 addition & 0 deletions src/inc/utilcode.h
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,7 @@ class CPUGroupInfo
};

int GetCurrentProcessCpuCount();
double GetCurrentProcessCpuBudget();
DWORD_PTR GetCurrentProcessCpuMask();

uint32_t GetOsPageSize();
Expand Down
2 changes: 1 addition & 1 deletion src/pal/inc/pal.h
Original file line number Diff line number Diff line change
Expand Up @@ -2454,7 +2454,7 @@ PAL_GetPhysicalMemoryUsed(size_t* val);
PALIMPORT
BOOL
PALAPI
PAL_GetCpuLimit(UINT* val);
PAL_GetCpuLimit(double* val);

PALIMPORT
size_t
Expand Down
16 changes: 8 additions & 8 deletions src/pal/src/misc/cgroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ class CGroup
return result;
}

static bool GetCpuLimit(UINT *val)
static bool GetCpuLimit(double *val)
{
long long quota;
long long period;
long long cpu_count;
double cpu_count;

quota = ReadCpuCGroupValue(CFS_QUOTA_FILENAME);
if (quota <= 0)
Expand All @@ -103,18 +103,18 @@ class CGroup
// Cannot have less than 1 CPU
if (quota <= period)
{
*val = 1;
*val = 1.0;
return true;
}
cpu_count = quota / period;
if (cpu_count < UINT_MAX)

cpu_count = (double) quota / period;
if (cpu_count < INT_MAX)
{
*val = cpu_count;
}
else
{
*val = UINT_MAX;
*val = INT_MAX;
}

return true;
Expand Down Expand Up @@ -465,7 +465,7 @@ PAL_GetPhysicalMemoryUsed(size_t* val)

BOOL
PALAPI
PAL_GetCpuLimit(UINT* val)
PAL_GetCpuLimit(double* val)
{
if (val == nullptr)
return FALSE;
Expand Down
27 changes: 21 additions & 6 deletions src/utilcode/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1219,16 +1219,31 @@ int GetCurrentProcessCpuCount()
count = 64;
}

cCPUs = count;

return count;
}

double GetCurrentProcessCpuBudget()
{
#ifdef FEATURE_PAL
uint32_t cpuLimit;
static double bCPUs = 0.0;

if (PAL_GetCpuLimit(&cpuLimit) && cpuLimit < count)
count = cpuLimit;
#endif
if (bCPUs != 0.0)
return bCPUs;

cCPUs = count;
double budget;
if (!PAL_GetCpuLimit(&budget))
{
budget = (double) GetCurrentProcessCpuCount();
}

return count;
bCPUs = budget;

return budget;
#else
return (double) GetCurrentProcessCpuCount();
#endif
}

DWORD_PTR GetCurrentProcessCpuMask()
Expand Down
7 changes: 7 additions & 0 deletions src/vm/gcenv.os.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,13 @@ uint32_t GCToOSInterface::GetCurrentProcessCpuCount()
return ::GetCurrentProcessCpuCount();
}

double GCToOSInterface::GetCurrentProcessCpuBudget()
{
LIMITED_METHOD_CONTRACT;

return ::GetCurrentProcessCpuBudget();
}

// Return the size of the user-mode portion of the virtual address space of this process.
// Return:
// non zero if it has succeeded, 0 if it has failed
Expand Down
16 changes: 15 additions & 1 deletion src/vm/win32threadpool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,23 @@ BOOL ThreadpoolMgr::Initialize()
//ThreadPool_CPUGroup
CPUGroupInfo::EnsureInitialized();
if (CPUGroupInfo::CanEnableGCCPUGroups() && CPUGroupInfo::CanEnableThreadUseAllCpuGroups())
{
NumberOfProcessors = CPUGroupInfo::GetNumActiveProcessors();
}
else
NumberOfProcessors = GetCurrentProcessCpuCount();
{
int cpuCount = GetCurrentProcessCpuCount();
double cpuBudget = GetCurrentProcessCpuBudget();
_ASSERTE(1.0 <= cpuBudget && cpuBudget < (double) INT_MAX);

if (cpuBudget < cpuCount)
{
cpuCount = (int)(cpuBudget + 0.5);
}

NumberOfProcessors = cpuCount;
}

InitPlatformVariables();

EX_TRY
Expand Down

0 comments on commit 58443dc

Please sign in to comment.