diff options
Diffstat (limited to 'Source/CTest')
-rw-r--r-- | Source/CTest/cmCTestBinPacker.cxx | 201 | ||||
-rw-r--r-- | Source/CTest/cmCTestBinPacker.h | 31 | ||||
-rw-r--r-- | Source/CTest/cmCTestHardwareAllocator.cxx | 86 | ||||
-rw-r--r-- | Source/CTest/cmCTestHardwareAllocator.h | 39 | ||||
-rw-r--r-- | Source/CTest/cmCTestHardwareSpec.cxx | 133 | ||||
-rw-r--r-- | Source/CTest/cmCTestHardwareSpec.h | 40 | ||||
-rw-r--r-- | Source/CTest/cmCTestMultiProcessHandler.cxx | 166 | ||||
-rw-r--r-- | Source/CTest/cmCTestMultiProcessHandler.h | 33 | ||||
-rw-r--r-- | Source/CTest/cmCTestProcessesLexerHelper.cxx | 55 | ||||
-rw-r--r-- | Source/CTest/cmCTestProcessesLexerHelper.h | 44 | ||||
-rw-r--r-- | Source/CTest/cmCTestRunTest.cxx | 43 | ||||
-rw-r--r-- | Source/CTest/cmCTestRunTest.h | 19 | ||||
-rw-r--r-- | Source/CTest/cmCTestTestCommand.cxx | 4 | ||||
-rw-r--r-- | Source/CTest/cmCTestTestCommand.h | 1 | ||||
-rw-r--r-- | Source/CTest/cmCTestTestHandler.cxx | 43 | ||||
-rw-r--r-- | Source/CTest/cmCTestTestHandler.h | 19 |
16 files changed, 950 insertions, 7 deletions
diff --git a/Source/CTest/cmCTestBinPacker.cxx b/Source/CTest/cmCTestBinPacker.cxx new file mode 100644 index 0000000..e9e3bee --- /dev/null +++ b/Source/CTest/cmCTestBinPacker.cxx @@ -0,0 +1,201 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmCTestBinPacker.h" + +#include <algorithm> +#include <utility> + +bool cmCTestBinPackerAllocation::operator==( + const cmCTestBinPackerAllocation& other) const +{ + return this->ProcessIndex == other.ProcessIndex && + this->SlotsNeeded == other.SlotsNeeded && this->Id == other.Id; +} + +bool cmCTestBinPackerAllocation::operator!=( + const cmCTestBinPackerAllocation& other) const +{ + return !(*this == other); +} + +namespace { + +/* + * The following algorithm is used to do two things: + * + * 1) Determine if a test's hardware requirements can fit within the hardware + * present on the system, and + * 2) Do the actual allocation + * + * This algorithm performs a recursive search, looking for a bin pack that will + * fit the specified requirements. It has a template to specify different + * optimization strategies. If it ever runs out of room, it backtracks as far + * down the stack as it needs to and tries a different combination until no + * more combinations can be tried. + */ +template <typename AllocationStrategy> +static bool AllocateCTestHardware( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + const std::vector<std::string>& hardwareSorted, std::size_t currentIndex, + std::vector<cmCTestBinPackerAllocation*>& allocations) +{ + // Iterate through all large enough resources until we find a solution + std::size_t hardwareIndex = 0; + while (hardwareIndex < hardwareSorted.size()) { + auto const& resource = hardware.at(hardwareSorted[hardwareIndex]); + if (resource.Free() >= + static_cast<unsigned int>(allocations[currentIndex]->SlotsNeeded)) { + // Preemptively allocate the resource + allocations[currentIndex]->Id = hardwareSorted[hardwareIndex]; + if (currentIndex + 1 >= allocations.size()) { + // We have a solution + return true; + } + + // Move the resource up the list until it is sorted again + auto hardware2 = hardware; + auto hardwareSorted2 = hardwareSorted; + hardware2[hardwareSorted2[hardwareIndex]].Locked += + allocations[currentIndex]->SlotsNeeded; + AllocationStrategy::IncrementalSort(hardware2, hardwareSorted2, + hardwareIndex); + + // Recurse one level deeper + if (AllocateCTestHardware<AllocationStrategy>( + hardware2, hardwareSorted2, currentIndex + 1, allocations)) { + return true; + } + } + + // No solution found here, deallocate the resource and try the next one + allocations[currentIndex]->Id.clear(); + auto freeSlots = hardware.at(hardwareSorted.at(hardwareIndex)).Free(); + do { + ++hardwareIndex; + } while (hardwareIndex < hardwareSorted.size() && + hardware.at(hardwareSorted.at(hardwareIndex)).Free() == + freeSlots); + } + + // No solution was found + return false; +} + +template <typename AllocationStrategy> +static bool AllocateCTestHardware( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<cmCTestBinPackerAllocation>& allocations) +{ + // Sort the resource requirements in descending order by slots needed + std::vector<cmCTestBinPackerAllocation*> allocationsPtr; + allocationsPtr.reserve(allocations.size()); + for (auto& allocation : allocations) { + allocationsPtr.push_back(&allocation); + } + std::stable_sort( + allocationsPtr.rbegin(), allocationsPtr.rend(), + [](cmCTestBinPackerAllocation* a1, cmCTestBinPackerAllocation* a2) { + return a1->SlotsNeeded < a2->SlotsNeeded; + }); + + // Sort the resources according to sort strategy + std::vector<std::string> hardwareSorted; + hardwareSorted.reserve(hardware.size()); + for (auto const& hw : hardware) { + hardwareSorted.push_back(hw.first); + } + AllocationStrategy::InitialSort(hardware, hardwareSorted); + + // Do the actual allocation + return AllocateCTestHardware<AllocationStrategy>( + hardware, hardwareSorted, std::size_t(0), allocationsPtr); +} + +class RoundRobinAllocationStrategy +{ +public: + static void InitialSort( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<std::string>& hardwareSorted); + + static void IncrementalSort( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<std::string>& hardwareSorted, std::size_t lastAllocatedIndex); +}; + +void RoundRobinAllocationStrategy::InitialSort( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<std::string>& hardwareSorted) +{ + std::stable_sort( + hardwareSorted.rbegin(), hardwareSorted.rend(), + [&hardware](const std::string& id1, const std::string& id2) { + return hardware.at(id1).Free() < hardware.at(id2).Free(); + }); +} + +void RoundRobinAllocationStrategy::IncrementalSort( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<std::string>& hardwareSorted, std::size_t lastAllocatedIndex) +{ + auto tmp = hardwareSorted[lastAllocatedIndex]; + std::size_t i = lastAllocatedIndex; + while (i < hardwareSorted.size() - 1 && + hardware.at(hardwareSorted[i + 1]).Free() > hardware.at(tmp).Free()) { + hardwareSorted[i] = hardwareSorted[i + 1]; + ++i; + } + hardwareSorted[i] = tmp; +} + +class BlockAllocationStrategy +{ +public: + static void InitialSort( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<std::string>& hardwareSorted); + + static void IncrementalSort( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<std::string>& hardwareSorted, std::size_t lastAllocatedIndex); +}; + +void BlockAllocationStrategy::InitialSort( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<std::string>& hardwareSorted) +{ + std::stable_sort( + hardwareSorted.rbegin(), hardwareSorted.rend(), + [&hardware](const std::string& id1, const std::string& id2) { + return hardware.at(id1).Free() < hardware.at(id2).Free(); + }); +} + +void BlockAllocationStrategy::IncrementalSort( + const std::map<std::string, cmCTestHardwareAllocator::Resource>&, + std::vector<std::string>& hardwareSorted, std::size_t lastAllocatedIndex) +{ + auto tmp = hardwareSorted[lastAllocatedIndex]; + std::size_t i = lastAllocatedIndex; + while (i > 0) { + hardwareSorted[i] = hardwareSorted[i - 1]; + --i; + } + hardwareSorted[i] = tmp; +} +} + +bool cmAllocateCTestHardwareRoundRobin( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<cmCTestBinPackerAllocation>& allocations) +{ + return AllocateCTestHardware<RoundRobinAllocationStrategy>(hardware, + allocations); +} + +bool cmAllocateCTestHardwareBlock( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<cmCTestBinPackerAllocation>& allocations) +{ + return AllocateCTestHardware<BlockAllocationStrategy>(hardware, allocations); +} diff --git a/Source/CTest/cmCTestBinPacker.h b/Source/CTest/cmCTestBinPacker.h new file mode 100644 index 0000000..54f03d7 --- /dev/null +++ b/Source/CTest/cmCTestBinPacker.h @@ -0,0 +1,31 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#ifndef cmCTestBinPacker_h +#define cmCTestBinPacker_h + +#include <cstddef> +#include <map> +#include <string> +#include <vector> + +#include "cmCTestHardwareAllocator.h" + +struct cmCTestBinPackerAllocation +{ + std::size_t ProcessIndex; + int SlotsNeeded; + std::string Id; + + bool operator==(const cmCTestBinPackerAllocation& other) const; + bool operator!=(const cmCTestBinPackerAllocation& other) const; +}; + +bool cmAllocateCTestHardwareRoundRobin( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<cmCTestBinPackerAllocation>& allocations); + +bool cmAllocateCTestHardwareBlock( + const std::map<std::string, cmCTestHardwareAllocator::Resource>& hardware, + std::vector<cmCTestBinPackerAllocation>& allocations); + +#endif diff --git a/Source/CTest/cmCTestHardwareAllocator.cxx b/Source/CTest/cmCTestHardwareAllocator.cxx new file mode 100644 index 0000000..2d1833d --- /dev/null +++ b/Source/CTest/cmCTestHardwareAllocator.cxx @@ -0,0 +1,86 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include "cmCTestHardwareAllocator.h" + +#include <utility> +#include <vector> + +#include "cmCTestHardwareSpec.h" + +void cmCTestHardwareAllocator::InitializeFromHardwareSpec( + const cmCTestHardwareSpec& spec) +{ + this->Resources.clear(); + + for (auto const& it : spec.LocalSocket.Resources) { + auto& res = this->Resources[it.first]; + for (auto const& specRes : it.second) { + res[specRes.Id].Total = specRes.Capacity; + res[specRes.Id].Locked = 0; + } + } +} + +const std::map<std::string, + std::map<std::string, cmCTestHardwareAllocator::Resource>>& +cmCTestHardwareAllocator::GetResources() const +{ + return this->Resources; +} + +bool cmCTestHardwareAllocator::AllocateResource(const std::string& name, + const std::string& id, + unsigned int slots) +{ + auto it = this->Resources.find(name); + if (it == this->Resources.end()) { + return false; + } + + auto resIt = it->second.find(id); + if (resIt == it->second.end()) { + return false; + } + + if (resIt->second.Total < resIt->second.Locked + slots) { + return false; + } + + resIt->second.Locked += slots; + return true; +} + +bool cmCTestHardwareAllocator::DeallocateResource(const std::string& name, + const std::string& id, + unsigned int slots) +{ + auto it = this->Resources.find(name); + if (it == this->Resources.end()) { + return false; + } + + auto resIt = it->second.find(id); + if (resIt == it->second.end()) { + return false; + } + + if (resIt->second.Locked < slots) { + return false; + } + + resIt->second.Locked -= slots; + return true; +} + +bool cmCTestHardwareAllocator::Resource::operator==( + const Resource& other) const +{ + return this->Total == other.Total && this->Locked == other.Locked; +} + +bool cmCTestHardwareAllocator::Resource::operator!=( + const Resource& other) const +{ + return !(*this == other); +} diff --git a/Source/CTest/cmCTestHardwareAllocator.h b/Source/CTest/cmCTestHardwareAllocator.h new file mode 100644 index 0000000..441f84d --- /dev/null +++ b/Source/CTest/cmCTestHardwareAllocator.h @@ -0,0 +1,39 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#ifndef cmCTestHardwareAllocator_h +#define cmCTestHardwareAllocator_h + +#include <map> +#include <string> + +class cmCTestHardwareSpec; + +class cmCTestHardwareAllocator +{ +public: + struct Resource + { + unsigned int Total; + unsigned int Locked; + + unsigned int Free() const { return this->Total - this->Locked; } + + bool operator==(const Resource& other) const; + bool operator!=(const Resource& other) const; + }; + + void InitializeFromHardwareSpec(const cmCTestHardwareSpec& spec); + + const std::map<std::string, std::map<std::string, Resource>>& GetResources() + const; + + bool AllocateResource(const std::string& name, const std::string& id, + unsigned int slots); + bool DeallocateResource(const std::string& name, const std::string& id, + unsigned int slots); + +private: + std::map<std::string, std::map<std::string, Resource>> Resources; +}; + +#endif diff --git a/Source/CTest/cmCTestHardwareSpec.cxx b/Source/CTest/cmCTestHardwareSpec.cxx new file mode 100644 index 0000000..137398a --- /dev/null +++ b/Source/CTest/cmCTestHardwareSpec.cxx @@ -0,0 +1,133 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmCTestHardwareSpec.h" + +#include <map> +#include <string> +#include <utility> +#include <vector> + +#include "cmsys/FStream.hxx" +#include "cmsys/RegularExpression.hxx" + +#include "cm_jsoncpp_reader.h" +#include "cm_jsoncpp_value.h" + +static const cmsys::RegularExpression IdentifierRegex{ "^[a-z_][a-z0-9_]*$" }; +static const cmsys::RegularExpression IdRegex{ "^[a-z0-9_]+$" }; + +bool cmCTestHardwareSpec::ReadFromJSONFile(const std::string& filename) +{ + cmsys::ifstream fin(filename.c_str()); + if (!fin) { + return false; + } + + Json::Value root; + Json::CharReaderBuilder builder; + if (!Json::parseFromStream(builder, fin, &root, nullptr)) { + return false; + } + + if (!root.isObject()) { + return false; + } + + auto const& local = root["local"]; + if (!local.isArray()) { + return false; + } + if (local.size() > 1) { + return false; + } + + if (local.empty()) { + this->LocalSocket.Resources.clear(); + return true; + } + + auto const& localSocket = local[0]; + if (!localSocket.isObject()) { + return false; + } + std::map<std::string, std::vector<cmCTestHardwareSpec::Resource>> resources; + cmsys::RegularExpressionMatch match; + for (auto const& key : localSocket.getMemberNames()) { + if (IdentifierRegex.find(key.c_str(), match)) { + auto const& value = localSocket[key]; + auto& r = resources[key]; + if (value.isArray()) { + for (auto const& item : value) { + if (item.isObject()) { + cmCTestHardwareSpec::Resource resource; + + if (!item.isMember("id")) { + return false; + } + auto const& id = item["id"]; + if (!id.isString()) { + return false; + } + resource.Id = id.asString(); + if (!IdRegex.find(resource.Id.c_str(), match)) { + return false; + } + + if (item.isMember("slots")) { + auto const& capacity = item["slots"]; + if (!capacity.isConvertibleTo(Json::uintValue)) { + return false; + } + resource.Capacity = capacity.asUInt(); + } else { + resource.Capacity = 1; + } + + r.push_back(resource); + } else { + return false; + } + } + } else { + return false; + } + } + } + + this->LocalSocket.Resources = std::move(resources); + return true; +} + +bool cmCTestHardwareSpec::operator==(const cmCTestHardwareSpec& other) const +{ + return this->LocalSocket == other.LocalSocket; +} + +bool cmCTestHardwareSpec::operator!=(const cmCTestHardwareSpec& other) const +{ + return !(*this == other); +} + +bool cmCTestHardwareSpec::Socket::operator==( + const cmCTestHardwareSpec::Socket& other) const +{ + return this->Resources == other.Resources; +} + +bool cmCTestHardwareSpec::Socket::operator!=( + const cmCTestHardwareSpec::Socket& other) const +{ + return !(*this == other); +} + +bool cmCTestHardwareSpec::Resource::operator==( + const cmCTestHardwareSpec::Resource& other) const +{ + return this->Id == other.Id && this->Capacity == other.Capacity; +} + +bool cmCTestHardwareSpec::Resource::operator!=( + const cmCTestHardwareSpec::Resource& other) const +{ + return !(*this == other); +} diff --git a/Source/CTest/cmCTestHardwareSpec.h b/Source/CTest/cmCTestHardwareSpec.h new file mode 100644 index 0000000..a0b4cae --- /dev/null +++ b/Source/CTest/cmCTestHardwareSpec.h @@ -0,0 +1,40 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#ifndef cmCTestHardwareSpec_h +#define cmCTestHardwareSpec_h + +#include <map> +#include <string> +#include <vector> + +class cmCTestHardwareSpec +{ +public: + class Resource + { + public: + std::string Id; + unsigned int Capacity; + + bool operator==(const Resource& other) const; + bool operator!=(const Resource& other) const; + }; + + class Socket + { + public: + std::map<std::string, std::vector<Resource>> Resources; + + bool operator==(const Socket& other) const; + bool operator!=(const Socket& other) const; + }; + + Socket LocalSocket; + + bool ReadFromJSONFile(const std::string& filename); + + bool operator==(const cmCTestHardwareSpec& other) const; + bool operator!=(const cmCTestHardwareSpec& other) const; +}; + +#endif diff --git a/Source/CTest/cmCTestMultiProcessHandler.cxx b/Source/CTest/cmCTestMultiProcessHandler.cxx index 1902500..7e8d548 100644 --- a/Source/CTest/cmCTestMultiProcessHandler.cxx +++ b/Source/CTest/cmCTestMultiProcessHandler.cxx @@ -3,8 +3,10 @@ #include "cmCTestMultiProcessHandler.h" #include <algorithm> +#include <cassert> #include <chrono> #include <cmath> +#include <cstddef> #include <cstdlib> #include <cstring> #include <iomanip> @@ -27,6 +29,7 @@ #include "cmAffinity.h" #include "cmAlgorithms.h" #include "cmCTest.h" +#include "cmCTestBinPacker.h" #include "cmCTestRunTest.h" #include "cmCTestTestHandler.h" #include "cmDuration.h" @@ -133,6 +136,12 @@ void cmCTestMultiProcessHandler::RunTests() uv_run(&this->Loop, UV_RUN_DEFAULT); uv_loop_close(&this->Loop); + if (!this->StopTimePassed) { + assert(this->Completed == this->Total); + assert(this->Tests.empty()); + } + assert(this->AllHardwareAvailable()); + this->MarkFinished(); this->UpdateCostData(); } @@ -168,6 +177,10 @@ bool cmCTestMultiProcessHandler::StartTestProcess(int test) } testRun->SetIndex(test); testRun->SetTestProperties(this->Properties[test]); + if (this->TestHandler->UseHardwareSpec) { + testRun->SetUseAllocatedHardware(true); + testRun->SetAllocatedHardware(this->AllocatedHardware[test]); + } // Find any failed dependencies for this test. We assume the more common // scenario has no failed tests, so make it the outer loop. @@ -179,7 +192,13 @@ bool cmCTestMultiProcessHandler::StartTestProcess(int test) // Always lock the resources we'll be using, even if we fail to set the // working directory because FinishTestProcess() will try to unlock them - this->LockResources(test); + this->AllocateResources(test); + + if (!this->TestsHaveSufficientHardware[test]) { + testRun->StartFailure("Insufficient hardware"); + this->FinishTestProcess(testRun, false); + return false; + } cmWorkingDirectory workdir(this->Properties[test]->Directory); if (workdir.Failed()) { @@ -199,6 +218,110 @@ bool cmCTestMultiProcessHandler::StartTestProcess(int test) return false; } +bool cmCTestMultiProcessHandler::AllocateHardware(int index) +{ + if (!this->TestHandler->UseHardwareSpec) { + return true; + } + + std::map<std::string, std::vector<cmCTestBinPackerAllocation>> allocations; + if (!this->TryAllocateHardware(index, allocations)) { + return false; + } + + auto& allocatedHardware = this->AllocatedHardware[index]; + allocatedHardware.resize(this->Properties[index]->Processes.size()); + for (auto const& it : allocations) { + for (auto const& alloc : it.second) { + bool result = this->HardwareAllocator.AllocateResource( + it.first, alloc.Id, alloc.SlotsNeeded); + (void)result; + assert(result); + allocatedHardware[alloc.ProcessIndex][it.first].push_back( + { alloc.Id, static_cast<unsigned int>(alloc.SlotsNeeded) }); + } + } + + return true; +} + +bool cmCTestMultiProcessHandler::TryAllocateHardware( + int index, + std::map<std::string, std::vector<cmCTestBinPackerAllocation>>& allocations) +{ + allocations.clear(); + + std::size_t processIndex = 0; + for (auto const& process : this->Properties[index]->Processes) { + for (auto const& requirement : process) { + for (int i = 0; i < requirement.UnitsNeeded; ++i) { + allocations[requirement.ResourceType].push_back( + { processIndex, requirement.SlotsNeeded, "" }); + } + } + ++processIndex; + } + + auto const& availableHardware = this->HardwareAllocator.GetResources(); + for (auto& it : allocations) { + if (!availableHardware.count(it.first)) { + return false; + } + if (!cmAllocateCTestHardwareRoundRobin(availableHardware.at(it.first), + it.second)) { + return false; + } + } + + return true; +} + +void cmCTestMultiProcessHandler::DeallocateHardware(int index) +{ + if (!this->TestHandler->UseHardwareSpec) { + return; + } + + { + auto& allocatedHardware = this->AllocatedHardware[index]; + for (auto const& processAlloc : allocatedHardware) { + for (auto const& it : processAlloc) { + auto resourceType = it.first; + for (auto const& it2 : it.second) { + bool success = this->HardwareAllocator.DeallocateResource( + resourceType, it2.Id, it2.Slots); + (void)success; + assert(success); + } + } + } + } + this->AllocatedHardware.erase(index); +} + +bool cmCTestMultiProcessHandler::AllHardwareAvailable() +{ + for (auto const& it : this->HardwareAllocator.GetResources()) { + for (auto const& it2 : it.second) { + if (it2.second.Locked != 0) { + return false; + } + } + } + + return true; +} + +void cmCTestMultiProcessHandler::CheckHardwareAvailable() +{ + for (auto test : this->SortedTests) { + std::map<std::string, std::vector<cmCTestBinPackerAllocation>> allocations; + this->TestsHaveSufficientHardware[test] = + !this->TestHandler->UseHardwareSpec || + this->TryAllocateHardware(test, allocations); + } +} + bool cmCTestMultiProcessHandler::CheckStopTimePassed() { if (!this->StopTimePassed) { @@ -223,7 +346,7 @@ void cmCTestMultiProcessHandler::SetStopTimePassed() } } -void cmCTestMultiProcessHandler::LockResources(int index) +void cmCTestMultiProcessHandler::AllocateResources(int index) { this->LockedResources.insert( this->Properties[index]->LockedResources.begin(), @@ -234,7 +357,7 @@ void cmCTestMultiProcessHandler::LockResources(int index) } } -void cmCTestMultiProcessHandler::UnlockResources(int index) +void cmCTestMultiProcessHandler::DeallocateResources(int index) { for (std::string const& i : this->Properties[index]->LockedResources) { this->LockedResources.erase(i); @@ -281,12 +404,20 @@ bool cmCTestMultiProcessHandler::StartTest(int test) } } + // Allocate hardware + if (this->TestsHaveSufficientHardware[test] && + !this->AllocateHardware(test)) { + this->DeallocateHardware(test); + return false; + } + // if there are no depends left then run this test if (this->Tests[test].empty()) { return this->StartTestProcess(test); } // This test was not able to start because it is waiting // on depends to run + this->DeallocateHardware(test); return false; } @@ -471,7 +602,8 @@ void cmCTestMultiProcessHandler::FinishTestProcess(cmCTestRunTest* runner, this->TestFinishMap[test] = true; this->TestRunningMap[test] = false; this->WriteCheckpoint(test); - this->UnlockResources(test); + this->DeallocateHardware(test); + this->DeallocateResources(test); this->RunningCount -= GetProcessorsUsed(test); for (auto p : properties->Affinity) { @@ -780,6 +912,28 @@ static Json::Value DumpTimeoutAfterMatch( return timeoutAfterMatch; } +static Json::Value DumpProcessesToJsonArray( + const std::vector< + std::vector<cmCTestTestHandler::cmCTestTestResourceRequirement>>& + processes) +{ + Json::Value jsonProcesses = Json::arrayValue; + for (auto const& it : processes) { + Json::Value jsonProcess = Json::objectValue; + Json::Value requirements = Json::arrayValue; + for (auto const& it2 : it) { + Json::Value res = Json::objectValue; + res[".type"] = it2.ResourceType; + // res[".units"] = it2.UnitsNeeded; // Intentionally commented out + res["slots"] = it2.SlotsNeeded; + requirements.append(res); + } + jsonProcess["requirements"] = requirements; + jsonProcesses.append(jsonProcess); + } + return jsonProcesses; +} + static Json::Value DumpCTestProperty(std::string const& name, Json::Value value) { @@ -851,6 +1005,10 @@ static Json::Value DumpCTestProperties( "PASS_REGULAR_EXPRESSION", DumpRegExToJsonArray(testProperties.RequiredRegularExpressions))); } + if (!testProperties.Processes.empty()) { + properties.append(DumpCTestProperty( + "PROCESSES", DumpProcessesToJsonArray(testProperties.Processes))); + } if (testProperties.WantAffinity) { properties.append( DumpCTestProperty("PROCESSOR_AFFINITY", testProperties.WantAffinity)); diff --git a/Source/CTest/cmCTestMultiProcessHandler.h b/Source/CTest/cmCTestMultiProcessHandler.h index be31c75..da716f0 100644 --- a/Source/CTest/cmCTestMultiProcessHandler.h +++ b/Source/CTest/cmCTestMultiProcessHandler.h @@ -14,10 +14,13 @@ #include "cm_uv.h" +#include "cmCTestHardwareAllocator.h" #include "cmCTestTestHandler.h" #include "cmUVHandlePtr.h" class cmCTest; +struct cmCTestBinPackerAllocation; +class cmCTestHardwareSpec; class cmCTestRunTest; /** \class cmCTestMultiProcessHandler @@ -44,6 +47,11 @@ public: : public std::map<int, cmCTestTestHandler::cmCTestTestProperties*> { }; + struct HardwareAllocation + { + std::string Id; + unsigned int Slots; + }; cmCTestMultiProcessHandler(); virtual ~cmCTestMultiProcessHandler(); @@ -79,6 +87,13 @@ public: void SetQuiet(bool b) { this->Quiet = b; } + void InitHardwareAllocator(const cmCTestHardwareSpec& spec) + { + this->HardwareAllocator.InitializeFromHardwareSpec(spec); + } + + void CheckHardwareAvailable(); + protected: // Start the next test or tests as many as are allowed by // ParallelLevel @@ -119,8 +134,17 @@ protected: bool CheckStopTimePassed(); void SetStopTimePassed(); - void LockResources(int index); - void UnlockResources(int index); + void AllocateResources(int index); + void DeallocateResources(int index); + + bool AllocateHardware(int index); + bool TryAllocateHardware( + int index, + std::map<std::string, std::vector<cmCTestBinPackerAllocation>>& + allocations); + void DeallocateHardware(int index); + bool AllHardwareAvailable(); + // map from test number to set of depend tests TestMap Tests; TestList SortedTests; @@ -141,6 +165,11 @@ protected: std::vector<std::string>* Failed; std::vector<std::string> LastTestsFailed; std::set<std::string> LockedResources; + std::map<int, + std::vector<std::map<std::string, std::vector<HardwareAllocation>>>> + AllocatedHardware; + std::map<int, bool> TestsHaveSufficientHardware; + cmCTestHardwareAllocator HardwareAllocator; std::vector<cmCTestTestHandler::cmCTestTestResult>* TestResults; size_t ParallelLevel; // max number of process that can be run at once unsigned long TestLoad; diff --git a/Source/CTest/cmCTestProcessesLexerHelper.cxx b/Source/CTest/cmCTestProcessesLexerHelper.cxx new file mode 100644 index 0000000..797164b --- /dev/null +++ b/Source/CTest/cmCTestProcessesLexerHelper.cxx @@ -0,0 +1,55 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmCTestProcessesLexerHelper.h" + +#include "cmCTestProcessesLexer.h" +#include "cmCTestTestHandler.h" + +cmCTestProcessesLexerHelper::cmCTestProcessesLexerHelper( + std::vector<std::vector<cmCTestTestHandler::cmCTestTestResourceRequirement>>& + output) + : Output(output) +{ +} + +bool cmCTestProcessesLexerHelper::ParseString(const std::string& value) +{ + yyscan_t lexer; + cmCTestProcesses_yylex_init_extra(this, &lexer); + + auto state = cmCTestProcesses_yy_scan_string(value.c_str(), lexer); + int retval = cmCTestProcesses_yylex(lexer); + cmCTestProcesses_yy_delete_buffer(state, lexer); + + cmCTestProcesses_yylex_destroy(lexer); + return retval == 0; +} + +void cmCTestProcessesLexerHelper::SetProcessCount(unsigned int count) +{ + this->ProcessCount = count; +} + +void cmCTestProcessesLexerHelper::SetResourceType(const std::string& type) +{ + this->ResourceType = type; +} + +void cmCTestProcessesLexerHelper::SetNeededSlots(int count) +{ + this->NeededSlots = count; +} + +void cmCTestProcessesLexerHelper::WriteRequirement() +{ + this->Process.push_back({ this->ResourceType, this->NeededSlots, 1 }); +} + +void cmCTestProcessesLexerHelper::WriteProcess() +{ + for (unsigned int i = 0; i < this->ProcessCount; ++i) { + this->Output.push_back(this->Process); + } + this->Process.clear(); + this->ProcessCount = 1; +} diff --git a/Source/CTest/cmCTestProcessesLexerHelper.h b/Source/CTest/cmCTestProcessesLexerHelper.h new file mode 100644 index 0000000..6c9289f --- /dev/null +++ b/Source/CTest/cmCTestProcessesLexerHelper.h @@ -0,0 +1,44 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#ifndef cmCTestProcessesLexerHelper_h +#define cmCTestProcessesLexerHelper_h + +#include <string> +#include <vector> + +#include "cmCTestTestHandler.h" + +class cmCTestProcessesLexerHelper +{ +public: + struct ParserType + { + }; + + cmCTestProcessesLexerHelper( + std::vector< + std::vector<cmCTestTestHandler::cmCTestTestResourceRequirement>>& + output); + ~cmCTestProcessesLexerHelper() = default; + + bool ParseString(const std::string& value); + + void SetProcessCount(unsigned int count); + void SetResourceType(const std::string& type); + void SetNeededSlots(int count); + void WriteRequirement(); + void WriteProcess(); + +private: + std::vector<std::vector<cmCTestTestHandler::cmCTestTestResourceRequirement>>& + Output; + + unsigned int ProcessCount = 1; + std::string ResourceType; + int NeededSlots; + std::vector<cmCTestTestHandler::cmCTestTestResourceRequirement> Process; +}; + +#define YY_EXTRA_TYPE cmCTestProcessesLexerHelper* + +#endif diff --git a/Source/CTest/cmCTestRunTest.cxx b/Source/CTest/cmCTestRunTest.cxx index 0188fe0..7f7f736 100644 --- a/Source/CTest/cmCTestRunTest.cxx +++ b/Source/CTest/cmCTestRunTest.cxx @@ -3,6 +3,7 @@ #include "cmCTestRunTest.h" #include <chrono> +#include <cstddef> #include <cstdint> #include <cstdio> #include <cstring> @@ -689,10 +690,52 @@ bool cmCTestRunTest::ForkProcess(cmDuration testTimeOut, bool explicitTimeout, cmSystemTools::AppendEnv(*environment); } + if (this->UseAllocatedHardware) { + this->SetupHardwareEnvironment(); + } else { + cmSystemTools::UnsetEnv("CTEST_PROCESS_COUNT"); + } + return this->TestProcess->StartProcess(this->MultiTestHandler.Loop, affinity); } +void cmCTestRunTest::SetupHardwareEnvironment() +{ + std::string processCount = "CTEST_PROCESS_COUNT="; + processCount += std::to_string(this->AllocatedHardware.size()); + cmSystemTools::PutEnv(processCount); + + std::size_t i = 0; + for (auto const& process : this->AllocatedHardware) { + std::string prefix = "CTEST_PROCESS_"; + prefix += std::to_string(i); + std::string resourceList = prefix + '='; + prefix += '_'; + bool firstType = true; + for (auto const& it : process) { + if (!firstType) { + resourceList += ','; + } + firstType = false; + auto resourceType = it.first; + resourceList += resourceType; + std::string var = prefix + cmSystemTools::UpperCase(resourceType) + '='; + bool firstName = true; + for (auto const& it2 : it.second) { + if (!firstName) { + var += ';'; + } + firstName = false; + var += "id:" + it2.Id + ",slots:" + std::to_string(it2.Slots); + } + cmSystemTools::PutEnv(var); + } + cmSystemTools::PutEnv(resourceList); + ++i; + } +} + void cmCTestRunTest::WriteLogOutputTop(size_t completed, size_t total) { std::ostringstream outputStream; diff --git a/Source/CTest/cmCTestRunTest.h b/Source/CTest/cmCTestRunTest.h index c770bac..085a6b8 100644 --- a/Source/CTest/cmCTestRunTest.h +++ b/Source/CTest/cmCTestRunTest.h @@ -5,6 +5,7 @@ #include "cmConfigure.h" // IWYU pragma: keep +#include <map> #include <memory> #include <set> #include <string> @@ -12,12 +13,12 @@ #include <stddef.h> +#include "cmCTestMultiProcessHandler.h" #include "cmCTestTestHandler.h" #include "cmDuration.h" #include "cmProcess.h" class cmCTest; -class cmCTestMultiProcessHandler; /** \class cmRunTest * \brief represents a single test to be run @@ -83,6 +84,16 @@ public: bool TimedOutForStopTime() const { return this->TimeoutIsForStopTime; } + void SetUseAllocatedHardware(bool use) { this->UseAllocatedHardware = use; } + void SetAllocatedHardware( + const std::vector< + std::map<std::string, + std::vector<cmCTestMultiProcessHandler::HardwareAllocation>>>& + hardware) + { + this->AllocatedHardware = hardware; + } + private: bool NeedsToRerun(); void DartProcessing(); @@ -94,6 +105,8 @@ private: // Run post processing of the process output for MemCheck void MemCheckPostProcess(); + void SetupHardwareEnvironment(); + // Returns "completed/total Test #Index: " std::string GetTestPrefix(size_t completed, size_t total) const; @@ -112,6 +125,10 @@ private: std::string StartTime; std::string ActualCommand; std::vector<std::string> Arguments; + bool UseAllocatedHardware = false; + std::vector<std::map< + std::string, std::vector<cmCTestMultiProcessHandler::HardwareAllocation>>> + AllocatedHardware; bool RunUntilFail; int NumberOfRunsLeft; bool RunAgain; diff --git a/Source/CTest/cmCTestTestCommand.cxx b/Source/CTest/cmCTestTestCommand.cxx index d200b40..5496353 100644 --- a/Source/CTest/cmCTestTestCommand.cxx +++ b/Source/CTest/cmCTestTestCommand.cxx @@ -32,6 +32,7 @@ void cmCTestTestCommand::BindArguments() this->Bind("SCHEDULE_RANDOM"_s, this->ScheduleRandom); this->Bind("STOP_TIME"_s, this->StopTime); this->Bind("TEST_LOAD"_s, this->TestLoad); + this->Bind("HARDWARE_SPEC_FILE"_s, this->HardwareSpecFile); } cmCTestGenericHandler* cmCTestTestCommand::InitializeHandler() @@ -87,6 +88,9 @@ cmCTestGenericHandler* cmCTestTestCommand::InitializeHandler() if (!this->ScheduleRandom.empty()) { handler->SetOption("ScheduleRandom", this->ScheduleRandom.c_str()); } + if (!this->HardwareSpecFile.empty()) { + handler->SetOption("HardwareSpecFile", this->HardwareSpecFile.c_str()); + } if (!this->StopTime.empty()) { this->CTest->SetStopTime(this->StopTime); } diff --git a/Source/CTest/cmCTestTestCommand.h b/Source/CTest/cmCTestTestCommand.h index cb65c0b..dc15279 100644 --- a/Source/CTest/cmCTestTestCommand.h +++ b/Source/CTest/cmCTestTestCommand.h @@ -58,6 +58,7 @@ protected: std::string ScheduleRandom; std::string StopTime; std::string TestLoad; + std::string HardwareSpecFile; }; #endif diff --git a/Source/CTest/cmCTestTestHandler.cxx b/Source/CTest/cmCTestTestHandler.cxx index 67d16af..2be62ae 100644 --- a/Source/CTest/cmCTestTestHandler.cxx +++ b/Source/CTest/cmCTestTestHandler.cxx @@ -29,6 +29,7 @@ #include "cmAlgorithms.h" #include "cmCTest.h" #include "cmCTestMultiProcessHandler.h" +#include "cmCTestProcessesLexerHelper.h" #include "cmDuration.h" #include "cmExecutionStatus.h" #include "cmGeneratedFileStream.h" @@ -288,6 +289,7 @@ cmCTestTestHandler::cmCTestTestHandler() this->UseIncludeRegExpFlag = false; this->UseExcludeRegExpFlag = false; this->UseExcludeRegExpFirst = false; + this->UseHardwareSpec = false; this->CustomMaximumPassedTestOutputSize = 1 * 1024; this->CustomMaximumFailedTestOutputSize = 300 * 1024; @@ -508,6 +510,16 @@ bool cmCTestTestHandler::ProcessOptions() } this->SetRerunFailed(cmIsOn(this->GetOption("RerunFailed"))); + val = this->GetOption("HardwareSpecFile"); + if (val) { + this->UseHardwareSpec = true; + if (!this->HardwareSpec.ReadFromJSONFile(val)) { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Could not read hardware spec file: " << val << std::endl); + return false; + } + } + return true; } @@ -1225,6 +1237,9 @@ void cmCTestTestHandler::ProcessDirectory(std::vector<std::string>& passed, } else { parallel->SetTestLoad(this->CTest->GetTestLoad()); } + if (this->UseHardwareSpec) { + parallel->InitHardwareAllocator(this->HardwareSpec); + } *this->LogFile << "Start testing: " << this->CTest->CurrentTime() << std::endl @@ -1268,6 +1283,7 @@ void cmCTestTestHandler::ProcessDirectory(std::vector<std::string>& passed, parallel->SetPassFailVectors(&passed, &failed); this->TestResults.clear(); parallel->SetTestResults(&this->TestResults); + parallel->CheckHardwareAvailable(); if (this->CTest->ShouldPrintLabels()) { parallel->PrintLabels(); @@ -1610,6 +1626,14 @@ std::string cmCTestTestHandler::FindExecutable( return fullPath; } +bool cmCTestTestHandler::ParseProcessesProperty( + const std::string& val, + std::vector<std::vector<cmCTestTestResourceRequirement>>& processes) +{ + cmCTestProcessesLexerHelper lexer(processes); + return lexer.ParseString(val); +} + void cmCTestTestHandler::GetListOfTests() { if (!this->IncludeLabelRegExp.empty()) { @@ -2179,6 +2203,11 @@ bool cmCTestTestHandler::SetTestsProperties( if (key == "PROCESSOR_AFFINITY") { rt.WantAffinity = cmIsOn(val); } + if (key == "PROCESSES") { + if (!ParseProcessesProperty(val, rt.Processes)) { + return false; + } + } if (key == "SKIP_RETURN_CODE") { rt.SkipReturnCode = atoi(val.c_str()); if (rt.SkipReturnCode < 0 || rt.SkipReturnCode > 255) { @@ -2356,3 +2385,17 @@ bool cmCTestTestHandler::AddTest(const std::vector<std::string>& args) this->TestList.push_back(test); return true; } + +bool cmCTestTestHandler::cmCTestTestResourceRequirement::operator==( + const cmCTestTestResourceRequirement& other) const +{ + return this->ResourceType == other.ResourceType && + this->SlotsNeeded == other.SlotsNeeded && + this->UnitsNeeded == other.UnitsNeeded; +} + +bool cmCTestTestHandler::cmCTestTestResourceRequirement::operator!=( + const cmCTestTestResourceRequirement& other) const +{ + return !(*this == other); +} diff --git a/Source/CTest/cmCTestTestHandler.h b/Source/CTest/cmCTestTestHandler.h index 1807a5c..525215c 100644 --- a/Source/CTest/cmCTestTestHandler.h +++ b/Source/CTest/cmCTestTestHandler.h @@ -19,6 +19,7 @@ #include "cmsys/RegularExpression.hxx" #include "cmCTestGenericHandler.h" +#include "cmCTestHardwareSpec.h" #include "cmDuration.h" #include "cmListFileCache.h" @@ -102,6 +103,16 @@ public: void Initialize() override; + struct cmCTestTestResourceRequirement + { + std::string ResourceType; + int SlotsNeeded; + int UnitsNeeded; + + bool operator==(const cmCTestTestResourceRequirement& other) const; + bool operator!=(const cmCTestTestResourceRequirement& other) const; + }; + // NOTE: This struct is Saved/Restored // in cmCTestTestHandler, if you add to this class // then you must add the new members to that code or @@ -147,6 +158,7 @@ public: std::set<std::string> FixturesCleanup; std::set<std::string> FixturesRequired; std::set<std::string> RequireSuccessDepends; + std::vector<std::vector<cmCTestTestResourceRequirement>> Processes; // Private test generator properties used to track backtraces cmListFileBacktrace Backtrace; }; @@ -190,6 +202,10 @@ public: std::vector<std::string>& extraPaths, std::vector<std::string>& failed); + static bool ParseProcessesProperty( + const std::string& val, + std::vector<std::vector<cmCTestTestResourceRequirement>>& processes); + using ListOfTests = std::vector<cmCTestTestProperties>; protected: @@ -320,6 +336,9 @@ private: cmsys::RegularExpression IncludeTestsRegularExpression; cmsys::RegularExpression ExcludeTestsRegularExpression; + bool UseHardwareSpec; + cmCTestHardwareSpec HardwareSpec; + void GenerateRegressionImages(cmXMLWriter& xml, const std::string& dart); cmsys::RegularExpression DartStuff1; void CheckLabelFilter(cmCTestTestProperties& it); |