Merge topic 'ctest-affinity'

6be53c6695 CTest: Add options to control test process affinity to CPUs
c5428d8db2 libuv: disable process affinity during CMake bootstrap
24de561a1a libuv: unix,win: add uv_spawn option to set child CPU affinity mask
43d6e5a71f libuv: misc: add function to get CPU affinity mask size

Acked-by: Kitware Robot <kwrobot@kitware.com>
Merge-request: !1814
This commit is contained in:
Brad King
2018-03-07 12:14:24 +00:00
committed by Kitware Robot
25 changed files with 345 additions and 6 deletions
+1
View File
@@ -348,6 +348,7 @@ Properties on Tests
/prop_test/LABELS
/prop_test/MEASUREMENT
/prop_test/PASS_REGULAR_EXPRESSION
/prop_test/PROCESSOR_AFFINITY
/prop_test/PROCESSORS
/prop_test/REQUIRED_FILES
/prop_test/RESOURCE_LOCK
+3
View File
@@ -2,6 +2,7 @@ PROCESSORS
----------
Set to specify how many process slots this test requires.
If not set, the default is ``1`` processor.
Denotes the number of processors that this test will require. This is
typically used for MPI tests, and should be used in conjunction with
@@ -11,3 +12,5 @@ This will also be used to display a weighted test timing result in label and
subproject summaries in the command line output of :manual:`ctest(1)`. The wall
clock time for the test run will be multiplied by this property to give a
better idea of how much cpu resource CTest allocated for the test.
See also the :prop_test:`PROCESSOR_AFFINITY` test property.
+11
View File
@@ -0,0 +1,11 @@
PROCESSOR_AFFINITY
------------------
Set to a true value to ask CTest to launch the test process with CPU affinity
for a fixed set of processors. If enabled and supported for the current
platform, CTest will choose a set of processors to place in the CPU affinity
mask when launching the test process. The number of processors in the set is
determined by the :prop_test:`PROCESSORS` test property or the number of
processors available to CTest, whichever is smaller. The set of processors
chosen will be disjoint from the processors assigned to other concurrently
running tests that also have the ``PROCESSOR_AFFINITY`` property enabled.
+6
View File
@@ -0,0 +1,6 @@
ctest-affinity
--------------
* A :prop_test:`PROCESSOR_AFFINITY` test property was added to request
that CTest run a test with CPU affinity for a set of processors
disjoint from other concurrently running tests with the property set.
+2
View File
@@ -131,6 +131,8 @@ set(SRCS
LexerParser/cmListFileLexer.c
LexerParser/cmListFileLexer.in.l
cmAffinity.cxx
cmAffinity.h
cmArchiveWrite.cxx
cmBase32.cxx
cmCacheManager.cxx
@@ -2,6 +2,7 @@
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmCTestMultiProcessHandler.h"
#include "cmAffinity.h"
#include "cmCTest.h"
#include "cmCTestRunTest.h"
#include "cmCTestScriptHandler.h"
@@ -53,6 +54,8 @@ cmCTestMultiProcessHandler::cmCTestMultiProcessHandler()
this->TestLoad = 0;
this->Completed = 0;
this->RunningCount = 0;
this->ProcessorsAvailable = cmAffinity::GetProcessorsAvailable();
this->HaveAffinity = this->ProcessorsAvailable.size();
this->StopTimePassed = false;
this->HasCycles = false;
this->SerialTestRunning = false;
@@ -127,6 +130,21 @@ bool cmCTestMultiProcessHandler::StartTestProcess(int test)
return false;
}
if (this->HaveAffinity && this->Properties[test]->WantAffinity) {
size_t needProcessors = this->GetProcessorsUsed(test);
if (needProcessors > this->ProcessorsAvailable.size()) {
return false;
}
std::vector<size_t> affinity;
affinity.reserve(needProcessors);
for (size_t i = 0; i < needProcessors; ++i) {
auto p = this->ProcessorsAvailable.begin();
affinity.push_back(*p);
this->ProcessorsAvailable.erase(p);
}
this->Properties[test]->Affinity = std::move(affinity);
}
cmCTestOptionalLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
"test " << test << "\n", this->Quiet);
this->TestRunningMap[test] = true; // mark the test as running
@@ -200,6 +218,11 @@ inline size_t cmCTestMultiProcessHandler::GetProcessorsUsed(int test)
if (processors > this->ParallelLevel) {
processors = this->ParallelLevel;
}
// Cap tests that want affinity to the maximum affinity available.
if (this->HaveAffinity && processors > this->HaveAffinity &&
this->Properties[test]->WantAffinity) {
processors = this->HaveAffinity;
}
return processors;
}
@@ -398,6 +421,11 @@ void cmCTestMultiProcessHandler::FinishTestProcess(cmCTestRunTest* runner,
this->UnlockResources(test);
this->RunningCount -= GetProcessorsUsed(test);
for (auto p : properties->Affinity) {
this->ProcessorsAvailable.insert(p);
}
properties->Affinity.clear();
delete runner;
if (started) {
this->StartNextTests();
@@ -119,6 +119,8 @@ protected:
// Number of tests that are complete
size_t Completed;
size_t RunningCount;
std::set<size_t> ProcessorsAvailable;
size_t HaveAffinity;
bool StopTimePassed;
// list of test properties (indices concurrent to the test map)
PropertiesMap Properties;
+6 -3
View File
@@ -515,7 +515,8 @@ bool cmCTestRunTest::StartTest(size_t total)
}
return this->ForkProcess(timeout, this->TestProperties->ExplicitTimeout,
&this->TestProperties->Environment);
&this->TestProperties->Environment,
&this->TestProperties->Affinity);
}
void cmCTestRunTest::ComputeArguments()
@@ -591,7 +592,8 @@ void cmCTestRunTest::DartProcessing()
}
bool cmCTestRunTest::ForkProcess(cmDuration testTimeOut, bool explicitTimeout,
std::vector<std::string>* environment)
std::vector<std::string>* environment,
std::vector<size_t>* affinity)
{
this->TestProcess = cm::make_unique<cmProcess>(*this);
this->TestProcess->SetId(this->Index);
@@ -637,7 +639,8 @@ bool cmCTestRunTest::ForkProcess(cmDuration testTimeOut, bool explicitTimeout,
cmSystemTools::AppendEnv(*environment);
}
return this->TestProcess->StartProcess(this->MultiTestHandler.Loop);
return this->TestProcess->StartProcess(this->MultiTestHandler.Loop,
affinity);
}
void cmCTestRunTest::WriteLogOutputTop(size_t completed, size_t total)
+2 -1
View File
@@ -83,7 +83,8 @@ private:
void DartProcessing();
void ExeNotFound(std::string exe);
bool ForkProcess(cmDuration testTimeOut, bool explicitTimeout,
std::vector<std::string>* environment);
std::vector<std::string>* environment,
std::vector<size_t>* affinity);
void WriteLogOutputTop(size_t completed, size_t total);
// Run post processing of the process output for MemCheck
void MemCheckPostProcess();
+4
View File
@@ -2165,6 +2165,9 @@ bool cmCTestTestHandler::SetTestsProperties(
rt.Processors = 1;
}
}
if (key == "PROCESSOR_AFFINITY") {
rt.WantAffinity = cmSystemTools::IsOn(val.c_str());
}
if (key == "SKIP_RETURN_CODE") {
rt.SkipReturnCode = atoi(val.c_str());
if (rt.SkipReturnCode < 0 || rt.SkipReturnCode > 255) {
@@ -2336,6 +2339,7 @@ bool cmCTestTestHandler::AddTest(const std::vector<std::string>& args)
test.ExplicitTimeout = false;
test.Cost = 0;
test.Processors = 1;
test.WantAffinity = false;
test.SkipReturnCode = -1;
test.PreviousRuns = 0;
if (this->UseIncludeRegExpFlag &&
+2
View File
@@ -130,6 +130,8 @@ public:
int Index;
// Requested number of process slots
int Processors;
bool WantAffinity;
std::vector<size_t> Affinity;
// return code of test which will mark test as "not run"
int SkipReturnCode;
std::vector<std::string> Environment;
+17 -1
View File
@@ -83,7 +83,7 @@ void cmProcess::SetCommandArguments(std::vector<std::string> const& args)
this->Arguments = args;
}
bool cmProcess::StartProcess(uv_loop_t& loop)
bool cmProcess::StartProcess(uv_loop_t& loop, std::vector<size_t>* affinity)
{
this->ProcessState = cmProcess::State::Error;
if (this->Command.empty()) {
@@ -138,6 +138,22 @@ bool cmProcess::StartProcess(uv_loop_t& loop)
options.stdio_count = 3; // in, out and err
options.exit_cb = &cmProcess::OnExitCB;
options.stdio = stdio;
#if !defined(CMAKE_USE_SYSTEM_LIBUV)
std::vector<char> cpumask;
if (affinity && !affinity->empty()) {
cpumask.resize(static_cast<size_t>(uv_cpumask_size()), 0);
for (auto p : *affinity) {
cpumask[p] = 1;
}
options.cpumask = cpumask.data();
options.cpumask_size = cpumask.size();
} else {
options.cpumask = nullptr;
options.cpumask_size = 0;
}
#else
static_cast<void>(affinity);
#endif
status =
uv_read_start(pipe_reader, &cmProcess::OnAllocateCB, &cmProcess::OnReadCB);
+1 -1
View File
@@ -36,7 +36,7 @@ public:
void ChangeTimeout(cmDuration t);
void ResetStartTime();
// Return true if the process starts
bool StartProcess(uv_loop_t& loop);
bool StartProcess(uv_loop_t& loop, std::vector<size_t>* affinity);
enum class State
{
+62
View File
@@ -0,0 +1,62 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmAffinity.h"
#include "cm_uv.h"
#ifndef CMAKE_USE_SYSTEM_LIBUV
#ifdef _WIN32
#define CM_HAVE_CPU_AFFINITY
#include <windows.h>
#elif defined(__linux__) || defined(__FreeBSD__)
#define CM_HAVE_CPU_AFFINITY
#include <pthread.h>
#include <sched.h>
#if defined(__FreeBSD__)
#include <pthread_np.h>
#include <sys/cpuset.h>
#include <sys/param.h>
#endif
#if defined(__linux__)
typedef cpu_set_t cm_cpuset_t;
#else
typedef cpuset_t cm_cpuset_t;
#endif
#endif
#endif
namespace cmAffinity {
std::set<size_t> GetProcessorsAvailable()
{
std::set<size_t> processorsAvailable;
#ifdef CM_HAVE_CPU_AFFINITY
int cpumask_size = uv_cpumask_size();
if (cpumask_size > 0) {
#ifdef _WIN32
DWORD_PTR procmask;
DWORD_PTR sysmask;
if (GetProcessAffinityMask(GetCurrentProcess(), &procmask, &sysmask) !=
0) {
for (int i = 0; i < cpumask_size; ++i) {
if (procmask & (((DWORD_PTR)1) << i)) {
processorsAvailable.insert(i);
}
}
}
#else
cm_cpuset_t cpuset;
CPU_ZERO(&cpuset); // NOLINT(clang-tidy)
if (pthread_getaffinity_np(pthread_self(), sizeof(cpuset), &cpuset) == 0) {
for (int i = 0; i < cpumask_size; ++i) {
if (CPU_ISSET(i, &cpuset)) {
processorsAvailable.insert(i);
}
}
}
#endif
}
#endif
return processorsAvailable;
}
}
+12
View File
@@ -0,0 +1,12 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#pragma once
#include "cmConfigure.h" // IWYU pragma: keep
#include <cstddef>
#include <set>
namespace cmAffinity {
std::set<size_t> GetProcessorsAvailable();
}
+3
View File
@@ -49,3 +49,6 @@ if(TEST_CompileCommandOutput)
endif()
add_subdirectory(PseudoMemcheck)
add_executable(testAffinity testAffinity.cxx)
target_link_libraries(testAffinity CMakeLib)
+18
View File
@@ -0,0 +1,18 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmAffinity.h"
#include <cstddef>
#include <iostream>
#include <set>
int main()
{
std::set<size_t> cpus = cmAffinity::GetProcessorsAvailable();
if (!cpus.empty()) {
std::cout << "CPU affinity mask count is '" << cpus.size() << "'.\n";
} else {
std::cout << "CPU affinity not supported on this platform.\n";
}
return 0;
}
+3
View File
@@ -339,6 +339,9 @@ add_RunCMake_test(CPackConfig)
add_RunCMake_test(CPackInstallProperties)
add_RunCMake_test(ExternalProject)
add_RunCMake_test(FetchContent)
if(NOT CMake_TEST_EXTERNAL_CMAKE)
set(CTestCommandLine_ARGS -DTEST_AFFINITY=$<TARGET_FILE:testAffinity>)
endif()
add_RunCMake_test(CTestCommandLine)
add_RunCMake_test(CacheNewline)
# Only run this test on unix platforms that support
@@ -141,3 +141,23 @@ function(run_TestOutputSize)
)
endfunction()
run_TestOutputSize()
function(run_TestAffinity)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/TestAffinity)
set(RunCMake_TEST_NO_CLEAN 1)
file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
# Create a test with affinity enabled. The default PROCESSORS
# value is 1, so our expected output checks that this is the
# number of processors in the mask.
file(WRITE "${RunCMake_TEST_BINARY_DIR}/CTestTestfile.cmake" "
add_test(Affinity \"${TEST_AFFINITY}\")
set_tests_properties(Affinity PROPERTIES PROCESSOR_AFFINITY ON)
")
# Run ctest with a large parallel level so that the value is
# not responsible for capping the number of processors available.
run_cmake_command(TestAffinity ${CMAKE_CTEST_COMMAND} -V -j 64)
endfunction()
if(TEST_AFFINITY)
run_TestAffinity()
endif()
@@ -0,0 +1 @@
1: CPU affinity (mask count is '1'|not supported on this platform)\.
+14
View File
@@ -925,6 +925,19 @@ typedef struct uv_process_options_s {
*/
uv_uid_t uid;
uv_gid_t gid;
/*
Libuv can set the child process' CPU affinity mask. This happens when
`cpumask` is non-NULL. It must point to an array of char values
of length `cpumask_size`, whose value must be at least that returned by
uv_cpumask_size(). Each byte in the mask can be either zero (false)
or non-zero (true) to indicate whether the corresponding processor at
that index is included.
If enabled on an unsupported platform, uv_spawn() will fail with
UV_ENOTSUP.
*/
char* cpumask;
size_t cpumask_size;
} uv_process_options_t;
/*
@@ -1094,6 +1107,7 @@ UV_EXTERN uv_pid_t uv_os_getppid(void);
UV_EXTERN int uv_cpu_info(uv_cpu_info_t** cpu_infos, int* count);
UV_EXTERN void uv_free_cpu_info(uv_cpu_info_t* cpu_infos, int count);
UV_EXTERN int uv_cpumask_size(void);
UV_EXTERN int uv_interface_addresses(uv_interface_address_t** addresses,
int* count);
+12
View File
@@ -40,6 +40,7 @@
#include <sys/uio.h> /* writev */
#include <sys/resource.h> /* getrusage */
#include <pwd.h>
#include <sched.h>
#ifdef __sun
# include <netdb.h> /* MAXHOSTNAMELEN on Solaris */
@@ -63,6 +64,8 @@
# include <sys/sysctl.h>
# include <sys/filio.h>
# include <sys/wait.h>
# include <sys/param.h>
# include <sys/cpuset.h>
# define UV__O_CLOEXEC O_CLOEXEC
# if defined(__FreeBSD__) && __FreeBSD__ >= 10
# define uv__accept4 accept4
@@ -1340,6 +1343,15 @@ int uv_os_gethostname(char* buffer, size_t* size) {
}
int uv_cpumask_size(void) {
#if defined(__linux__) || defined(__FreeBSD__)
return CPU_SETSIZE;
#else
return UV_ENOTSUP;
#endif
}
uv_os_fd_t uv_get_osfhandle(int fd) {
return fd;
}
+55
View File
@@ -32,6 +32,7 @@
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <sched.h>
#if defined(__APPLE__) && !TARGET_OS_IPHONE
# include <crt_externs.h>
@@ -44,6 +45,16 @@ extern char **environ;
# include <grp.h>
#endif
#ifndef CMAKE_BOOTSTRAP
#if defined(__linux__)
# define uv__cpu_set_t cpu_set_t
#elif defined(__FreeBSD__)
# include <sys/param.h>
# include <sys/cpuset.h>
# include <pthread_np.h>
# define uv__cpu_set_t cpuset_t
#endif
#endif
static void uv__chld(uv_signal_t* handle, int signum) {
uv_process_t* process;
@@ -285,6 +296,14 @@ static void uv__process_child_init(const uv_process_options_t* options,
int err;
int fd;
int n;
#ifndef CMAKE_BOOTSTRAP
#if defined(__linux__) || defined(__FreeBSD__)
int r;
int i;
int cpumask_size;
uv__cpu_set_t cpuset;
#endif
#endif
if (options->flags & UV_PROCESS_DETACHED)
setsid();
@@ -375,6 +394,28 @@ static void uv__process_child_init(const uv_process_options_t* options,
_exit(127);
}
#ifndef CMAKE_BOOTSTRAP
#if defined(__linux__) || defined(__FreeBSD__)
if (options->cpumask != NULL) {
cpumask_size = uv_cpumask_size();
assert(options->cpumask_size >= (size_t)cpumask_size);
CPU_ZERO(&cpuset);
for (i = 0; i < cpumask_size; ++i) {
if (options->cpumask[i]) {
CPU_SET(i, &cpuset);
}
}
r = -pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
if (r != 0) {
uv__write_int(error_fd, r);
_exit(127);
}
}
#endif
#endif
if (options->env != NULL) {
environ = options->env;
}
@@ -429,6 +470,20 @@ int uv_spawn(uv_loop_t* loop,
int i;
int status;
if (options->cpumask != NULL) {
#ifndef CMAKE_BOOTSTRAP
#if defined(__linux__) || defined(__FreeBSD__)
if (options->cpumask_size < (size_t)uv_cpumask_size()) {
return UV_EINVAL;
}
#else
return UV_ENOTSUP;
#endif
#else
return UV_ENOTSUP;
#endif
}
assert(options->file != NULL);
assert(!(options->flags & ~(UV_PROCESS_DETACHED |
UV_PROCESS_SETGID |
+4
View File
@@ -603,3 +603,7 @@ int uv__socket_sockopt(uv_handle_t* handle, int optname, int* value) {
return 0;
}
int uv_cpumask_size(void) {
return (int)(sizeof(DWORD_PTR) * 8);
}
+56
View File
@@ -954,6 +954,12 @@ int uv_spawn(uv_loop_t* loop,
return UV_EINVAL;
}
if (options->cpumask != NULL) {
if (options->cpumask_size < (size_t)uv_cpumask_size()) {
return UV_EINVAL;
}
}
assert(options->file != NULL);
assert(!(options->flags & ~(UV_PROCESS_DETACHED |
UV_PROCESS_SETGID |
@@ -1084,6 +1090,12 @@ int uv_spawn(uv_loop_t* loop,
process_flags |= DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP;
}
if (options->cpumask != NULL) {
/* Create the child in a suspended state so we have a chance to set
its process affinity before it runs. */
process_flags |= CREATE_SUSPENDED;
}
if (!CreateProcessW(application_path,
arguments,
NULL,
@@ -1099,6 +1111,50 @@ int uv_spawn(uv_loop_t* loop,
goto done;
}
if (options->cpumask != NULL) {
/* The child is currently suspended. Set its process affinity
or terminate it if we can't. */
int i;
int cpumasksize;
DWORD_PTR sysmask;
DWORD_PTR oldmask;
DWORD_PTR newmask;
cpumasksize = uv_cpumask_size();
if (!GetProcessAffinityMask(info.hProcess, &oldmask, &sysmask)) {
err = GetLastError();
TerminateProcess(info.hProcess, 1);
goto done;
}
newmask = 0;
for (i = 0; i < cpumasksize; i++) {
if (options->cpumask[i]) {
if (oldmask & (((DWORD_PTR)1) << i)) {
newmask |= ((DWORD_PTR)1) << i;
} else {
err = UV_EINVAL;
TerminateProcess(info.hProcess, 1);
goto done;
}
}
}
if (!SetProcessAffinityMask(info.hProcess, newmask)) {
err = GetLastError();
TerminateProcess(info.hProcess, 1);
goto done;
}
/* The process affinity of the child is set. Let it run. */
if (ResumeThread(info.hThread) == ((DWORD)-1)) {
err = GetLastError();
TerminateProcess(info.hProcess, 1);
goto done;
}
}
/* Spawn succeeded */
/* Beyond this point, failure is reported asynchronously. */