mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
Use a named semaphore polled from the libuv event loop to share GNU Make job slots on Windows. Cover token ordering, bounded draining, and teardown, and document the new support. Fixes: #27968
510 lines
12 KiB
C++
510 lines
12 KiB
C++
#include <cassert>
|
|
#include <cstddef>
|
|
#include <deque>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <vector>
|
|
|
|
#include <cm/optional>
|
|
|
|
#include <cm3p/uv.h>
|
|
|
|
#ifdef _WIN32
|
|
# include <windows.h>
|
|
#else
|
|
# include <unistd.h>
|
|
#endif
|
|
|
|
#include "cmGetPipes.h"
|
|
#include "cmStringAlgorithms.h"
|
|
#include "cmSystemTools.h"
|
|
#include "cmUVHandlePtr.h"
|
|
#include "cmUVJobServerClient.h"
|
|
|
|
namespace {
|
|
|
|
std::size_t const kTOTAL_JOBS = 10;
|
|
std::size_t const kTOTAL_TOKENS = 3;
|
|
|
|
#ifdef _WIN32
|
|
std::size_t NextSemaphoreId = 0;
|
|
|
|
struct JobServerSemaphore
|
|
{
|
|
std::string Name;
|
|
HANDLE Handle = nullptr;
|
|
|
|
JobServerSemaphore(LONG initialCount, LONG maximumCount)
|
|
: Name(cmStrCat("cmake_test_jobserver_", GetCurrentProcessId(), '_',
|
|
++NextSemaphoreId))
|
|
{
|
|
SetLastError(ERROR_SUCCESS);
|
|
this->Handle = CreateSemaphoreA(nullptr, initialCount, maximumCount,
|
|
this->Name.c_str());
|
|
if (this->Handle && GetLastError() == ERROR_ALREADY_EXISTS) {
|
|
CloseHandle(this->Handle);
|
|
this->Handle = nullptr;
|
|
}
|
|
}
|
|
|
|
~JobServerSemaphore()
|
|
{
|
|
if (this->Handle) {
|
|
CloseHandle(this->Handle);
|
|
}
|
|
}
|
|
|
|
JobServerSemaphore(JobServerSemaphore const&) = delete;
|
|
JobServerSemaphore& operator=(JobServerSemaphore const&) = delete;
|
|
|
|
explicit operator bool() const { return this->Handle != nullptr; }
|
|
|
|
bool CountAvailableTokens(std::size_t& count) const
|
|
{
|
|
count = 0;
|
|
for (;;) {
|
|
DWORD const result = WaitForSingleObject(this->Handle, 0);
|
|
if (result == WAIT_OBJECT_0) {
|
|
++count;
|
|
} else if (result == WAIT_TIMEOUT) {
|
|
break;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return count == 0 ||
|
|
ReleaseSemaphore(this->Handle, static_cast<LONG>(count), nullptr);
|
|
}
|
|
};
|
|
|
|
struct MakeFlagsGuard
|
|
{
|
|
cm::optional<std::string> Original = cmSystemTools::GetEnvVar("MAKEFLAGS");
|
|
|
|
~MakeFlagsGuard()
|
|
{
|
|
if (this->Original) {
|
|
cmSystemTools::PutEnv(cmStrCat("MAKEFLAGS=", *this->Original));
|
|
} else {
|
|
cmSystemTools::UnsetEnv("MAKEFLAGS");
|
|
}
|
|
}
|
|
};
|
|
|
|
void SetJobServer(JobServerSemaphore const& jobServer)
|
|
{
|
|
cmSystemTools::PutEnv(cmStrCat("MAKEFLAGS=--flags-before"
|
|
" --jobserver-auth=bogus"
|
|
" --flags-between"
|
|
" --jobserver-auth=",
|
|
jobServer.Name, " --flags-after"));
|
|
}
|
|
|
|
bool CheckTokenCount(JobServerSemaphore const& jobServer, std::size_t expected)
|
|
{
|
|
std::size_t count;
|
|
if (!jobServer.CountAvailableTokens(count)) {
|
|
std::cerr << "Failed to inspect job server semaphore\n";
|
|
return false;
|
|
}
|
|
if (count != expected) {
|
|
std::cerr << "Expected " << expected << " job server tokens, got " << count
|
|
<< '\n';
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
struct Job
|
|
{
|
|
cm::uv_timer_ptr Timer;
|
|
};
|
|
|
|
struct JobRunner
|
|
{
|
|
cm::uv_loop_ptr Loop;
|
|
cm::optional<cmUVJobServerClient> JSC;
|
|
std::vector<Job> Jobs;
|
|
std::size_t NextJobIndex = 0;
|
|
|
|
std::size_t ActiveJobs = 0;
|
|
|
|
std::deque<std::size_t> Queue;
|
|
|
|
bool Okay = true;
|
|
|
|
JobRunner()
|
|
: Jobs(kTOTAL_JOBS)
|
|
{
|
|
this->Loop.init(nullptr);
|
|
this->JSC = cmUVJobServerClient::Connect(
|
|
*this->Loop, [this]() { this->StartQueuedJob(); }, nullptr);
|
|
if (!this->JSC) {
|
|
std::cerr << "Failed to connect to job server.\n";
|
|
this->Okay = false;
|
|
}
|
|
}
|
|
|
|
~JobRunner() {}
|
|
|
|
bool Run()
|
|
{
|
|
if (this->Okay) {
|
|
this->QueueNextJobs();
|
|
uv_run(this->Loop, UV_RUN_DEFAULT);
|
|
std::cerr << "HeldTokens: " << this->JSC->GetHeldTokens() << '\n';
|
|
std::cerr << "NeedTokens: " << this->JSC->GetNeedTokens() << '\n';
|
|
}
|
|
return this->Okay;
|
|
}
|
|
|
|
void QueueNextJobs()
|
|
{
|
|
std::cerr << "QueueNextJobs()\n";
|
|
std::size_t queued = 0;
|
|
while (queued < 2 && this->NextJobIndex < this->Jobs.size()) {
|
|
this->QueueJob(this->NextJobIndex);
|
|
++this->NextJobIndex;
|
|
++queued;
|
|
}
|
|
std::cerr << "QueueNextJobs done\n";
|
|
}
|
|
|
|
void StartQueuedJob()
|
|
{
|
|
std::cerr << "StartQueuedJob()\n";
|
|
assert(!this->Queue.empty());
|
|
|
|
std::size_t index = this->Queue.front();
|
|
this->Queue.pop_front();
|
|
this->StartJob(index);
|
|
|
|
std::cerr << "StartQueuedJob done\n";
|
|
}
|
|
|
|
void StartJob(std::size_t index)
|
|
{
|
|
cm::uv_timer_ptr& job = this->Jobs[index].Timer;
|
|
job.init(*this->Loop, this);
|
|
uv_timer_start(
|
|
job,
|
|
[](uv_timer_t* handle) {
|
|
uv_timer_stop(handle);
|
|
auto self = static_cast<JobRunner*>(handle->data);
|
|
self->FinishJob();
|
|
},
|
|
/*timeout_ms=*/10 * (1 + (index % 3)), /*repeat_ms=*/0);
|
|
++this->ActiveJobs;
|
|
std::cerr << " StartJob(" << index
|
|
<< "): Active jobs: " << this->ActiveJobs << '\n';
|
|
|
|
if (this->ActiveJobs > kTOTAL_TOKENS) {
|
|
std::cerr << "Started more than " << kTOTAL_TOKENS << " jobs at once!\n";
|
|
this->Okay = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
void QueueJob(std::size_t index)
|
|
{
|
|
this->JSC->RequestToken();
|
|
this->Queue.push_back(index);
|
|
std::cerr << " QueueJob(" << index
|
|
<< "): Queue length: " << this->Queue.size() << '\n';
|
|
}
|
|
|
|
void FinishJob()
|
|
{
|
|
--this->ActiveJobs;
|
|
std::cerr << "FinishJob: Active jobs: " << this->ActiveJobs << '\n';
|
|
|
|
this->JSC->ReleaseToken();
|
|
this->QueueNextJobs();
|
|
}
|
|
};
|
|
|
|
bool testJobServer()
|
|
{
|
|
#ifdef _WIN32
|
|
JobServerSemaphore jobServer(kTOTAL_TOKENS - 1, kTOTAL_TOKENS - 1);
|
|
if (!jobServer) {
|
|
std::cerr << "Failed to create job server semaphore\n";
|
|
return false;
|
|
}
|
|
SetJobServer(jobServer);
|
|
#else
|
|
// Create a job server pipe.
|
|
int jobServerPipe[2];
|
|
if (cmGetPipes(jobServerPipe) < 0) {
|
|
std::cerr << "Failed to create job server pipe\n";
|
|
return false;
|
|
}
|
|
|
|
// Write N-1 tokens to the pipe.
|
|
std::vector<char> jobServerInit(kTOTAL_TOKENS - 1, '.');
|
|
if (write(jobServerPipe[1], jobServerInit.data(), jobServerInit.size()) !=
|
|
kTOTAL_TOKENS - 1) {
|
|
std::cerr << "Failed to initialize job server pipe\n";
|
|
return false;
|
|
}
|
|
|
|
// Establish the job server client context.
|
|
// Add a bogus server spec to verify we use the last spec.
|
|
cmSystemTools::PutEnv(cmStrCat("MAKEFLAGS=--flags-before"
|
|
" --jobserver-auth=bogus"
|
|
" --flags-between"
|
|
" --jobserver-fds=",
|
|
jobServerPipe[0], ',', jobServerPipe[1],
|
|
" --flags-after"));
|
|
#endif
|
|
|
|
bool passed;
|
|
{
|
|
JobRunner jobRunner;
|
|
passed = jobRunner.Run();
|
|
}
|
|
#ifdef _WIN32
|
|
passed = CheckTokenCount(jobServer, kTOTAL_TOKENS - 1) && passed;
|
|
#endif
|
|
return passed;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
bool ConnectsWithMakeFlags(std::string const& makeFlags)
|
|
{
|
|
cmSystemTools::PutEnv(cmStrCat("MAKEFLAGS=", makeFlags));
|
|
cm::uv_loop_ptr loop;
|
|
if (loop.init(nullptr) != 0) {
|
|
return false;
|
|
}
|
|
cm::optional<cmUVJobServerClient> client =
|
|
cmUVJobServerClient::Connect(*loop, nullptr, nullptr);
|
|
return client.has_value();
|
|
}
|
|
|
|
bool testJobServerParsing()
|
|
{
|
|
JobServerSemaphore jobServer(1, 1);
|
|
if (!jobServer) {
|
|
std::cerr << "Failed to create parsing test semaphore\n";
|
|
return false;
|
|
}
|
|
|
|
bool passed = true;
|
|
passed =
|
|
ConnectsWithMakeFlags(cmStrCat("--jobserver-auth=", jobServer.Name)) &&
|
|
passed;
|
|
passed = ConnectsWithMakeFlags(cmStrCat("--jobserver-auth=bogus "
|
|
"--jobserver-fds=1,2 "
|
|
"--jobserver-auth=",
|
|
jobServer.Name)) &&
|
|
passed;
|
|
passed = !ConnectsWithMakeFlags(cmStrCat("--jobserver-auth=", jobServer.Name,
|
|
" --jobserver-fds=1,2")) &&
|
|
passed;
|
|
passed = !ConnectsWithMakeFlags("--jobserver-auth=fifo:somewhere") && passed;
|
|
passed = !ConnectsWithMakeFlags("--jobserver-auth=") && passed;
|
|
cmSystemTools::UnsetEnv("MAKEFLAGS");
|
|
cm::uv_loop_ptr loop;
|
|
if (loop.init(nullptr) != 0) {
|
|
return false;
|
|
}
|
|
passed = !cmUVJobServerClient::Connect(*loop, nullptr, nullptr) && passed;
|
|
|
|
if (!passed) {
|
|
std::cerr << "Job server MAKEFLAGS parsing test failed\n";
|
|
}
|
|
return passed;
|
|
}
|
|
|
|
bool testDeferredImplicitTokenOrdering()
|
|
{
|
|
JobServerSemaphore jobServer(1, 1);
|
|
if (!jobServer) {
|
|
return false;
|
|
}
|
|
SetJobServer(jobServer);
|
|
|
|
cm::uv_loop_ptr loop;
|
|
if (loop.init(nullptr) != 0) {
|
|
return false;
|
|
}
|
|
|
|
std::size_t deliveries = 0;
|
|
cm::optional<cmUVJobServerClient> client;
|
|
client = cmUVJobServerClient::Connect(
|
|
*loop,
|
|
[&]() {
|
|
++deliveries;
|
|
client->ReleaseToken();
|
|
},
|
|
nullptr);
|
|
if (!client) {
|
|
return false;
|
|
}
|
|
|
|
client->RequestToken();
|
|
client->RequestToken();
|
|
bool passed = deliveries == 0;
|
|
uv_run(loop, UV_RUN_DEFAULT);
|
|
|
|
passed = deliveries == 2 && client->GetHeldTokens() == 0 &&
|
|
client->GetNeedTokens() == 0 && CheckTokenCount(jobServer, 1) && passed;
|
|
if (!passed) {
|
|
std::cerr << "Deferred implicit-token ordering test failed\n";
|
|
}
|
|
return passed;
|
|
}
|
|
|
|
bool testTimerRestartFromCallback()
|
|
{
|
|
JobServerSemaphore jobServer(2, 2);
|
|
if (!jobServer) {
|
|
return false;
|
|
}
|
|
SetJobServer(jobServer);
|
|
|
|
cm::uv_loop_ptr loop;
|
|
if (loop.init(nullptr) != 0) {
|
|
return false;
|
|
}
|
|
|
|
std::size_t deliveries = 0;
|
|
cm::optional<cmUVJobServerClient> client;
|
|
client = cmUVJobServerClient::Connect(
|
|
*loop,
|
|
[&]() {
|
|
++deliveries;
|
|
if (deliveries == 2) {
|
|
client->ReleaseToken();
|
|
client->RequestToken();
|
|
} else if (deliveries == 3) {
|
|
client->ReleaseToken();
|
|
}
|
|
},
|
|
nullptr);
|
|
if (!client) {
|
|
return false;
|
|
}
|
|
|
|
client->RequestToken();
|
|
uv_run(loop, UV_RUN_DEFAULT);
|
|
bool passed = deliveries == 1 && client->GetHeldTokens() == 1;
|
|
|
|
client->RequestToken();
|
|
uv_run(loop, UV_RUN_DEFAULT);
|
|
passed = deliveries == 3 && client->GetHeldTokens() == 1 &&
|
|
client->GetNeedTokens() == 0 && passed;
|
|
client->ReleaseToken();
|
|
passed = CheckTokenCount(jobServer, 2) && passed;
|
|
if (!passed) {
|
|
std::cerr << "Timer restart test failed\n";
|
|
}
|
|
return passed;
|
|
}
|
|
|
|
bool testBoundedTokenDrain()
|
|
{
|
|
std::size_t const explicitTokens = 40;
|
|
JobServerSemaphore jobServer(explicitTokens, explicitTokens);
|
|
if (!jobServer) {
|
|
return false;
|
|
}
|
|
SetJobServer(jobServer);
|
|
|
|
cm::uv_loop_ptr loop;
|
|
if (loop.init(nullptr) != 0) {
|
|
return false;
|
|
}
|
|
|
|
std::size_t deliveries = 0;
|
|
cm::optional<cmUVJobServerClient> client =
|
|
cmUVJobServerClient::Connect(*loop, [&]() { ++deliveries; }, nullptr);
|
|
if (!client) {
|
|
return false;
|
|
}
|
|
|
|
client->RequestToken();
|
|
uv_run(loop, UV_RUN_DEFAULT);
|
|
for (std::size_t i = 0; i < explicitTokens; ++i) {
|
|
client->RequestToken();
|
|
}
|
|
|
|
uv_run(loop, UV_RUN_NOWAIT);
|
|
bool passed = deliveries == 33 && client->GetNeedTokens() == 8;
|
|
uv_run(loop, UV_RUN_DEFAULT);
|
|
passed = deliveries == explicitTokens + 1 &&
|
|
client->GetHeldTokens() == static_cast<int>(explicitTokens + 1) &&
|
|
client->GetNeedTokens() == 0 && passed;
|
|
|
|
for (std::size_t i = 0; i < explicitTokens + 1; ++i) {
|
|
client->ReleaseToken();
|
|
}
|
|
passed = CheckTokenCount(jobServer, explicitTokens) && passed;
|
|
if (!passed) {
|
|
std::cerr << "Bounded token drain test failed\n";
|
|
}
|
|
return passed;
|
|
}
|
|
|
|
bool testPendingRequestTeardown()
|
|
{
|
|
JobServerSemaphore jobServer(0, 1);
|
|
if (!jobServer) {
|
|
return false;
|
|
}
|
|
SetJobServer(jobServer);
|
|
|
|
cm::uv_loop_ptr loop;
|
|
if (loop.init(nullptr) != 0) {
|
|
return false;
|
|
}
|
|
|
|
std::size_t deliveries = 0;
|
|
{
|
|
cm::optional<cmUVJobServerClient> client =
|
|
cmUVJobServerClient::Connect(*loop, [&]() { ++deliveries; }, nullptr);
|
|
if (!client) {
|
|
return false;
|
|
}
|
|
|
|
client->RequestToken();
|
|
uv_run(loop, UV_RUN_NOWAIT);
|
|
client->RequestToken();
|
|
uv_run(loop, UV_RUN_NOWAIT);
|
|
if (deliveries != 1 || client->GetHeldTokens() != 1 ||
|
|
client->GetNeedTokens() != 1) {
|
|
std::cerr << "Pending request setup failed\n";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
uv_run(loop, UV_RUN_DEFAULT);
|
|
bool const passed = CheckTokenCount(jobServer, 0);
|
|
if (!passed) {
|
|
std::cerr << "Pending request teardown test failed\n";
|
|
}
|
|
return passed;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
int testUVJobServerClient(int, char** const)
|
|
{
|
|
#ifdef _WIN32
|
|
MakeFlagsGuard makeFlagsGuard;
|
|
#endif
|
|
bool passed = true;
|
|
passed = testJobServer() && passed;
|
|
#ifdef _WIN32
|
|
passed = testJobServerParsing() && passed;
|
|
passed = testDeferredImplicitTokenOrdering() && passed;
|
|
passed = testTimerRestartFromCallback() && passed;
|
|
passed = testBoundedTokenDrain() && passed;
|
|
passed = testPendingRequestTeardown() && passed;
|
|
#endif
|
|
return passed ? 0 : -1;
|
|
}
|