AnyPS5/core/libs/tests/GuestMutexContention.cpp

201 lines
8.9 KiB
C++

#include "SceTypes.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <thread>
#include <vector>
#include <windows.h>
extern "C" {
int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*);
int APS5_VABI scePthreadJoin(Pthread, void**);
int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr*);
int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr*, int);
int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr*);
int APS5_VABI scePthreadMutexInit(PthreadMutex*, const PthreadMutexattr*, const char*);
int APS5_VABI scePthreadMutexDestroy(PthreadMutex*);
int APS5_VABI scePthreadMutexLock(PthreadMutex*);
int APS5_VABI scePthreadMutexTimedlock(PthreadMutex*, KernelUseconds);
int APS5_VABI scePthreadMutexUnlock(PthreadMutex*);
}
namespace {
using Clock = std::chrono::steady_clock;
constexpr int TimedOut = static_cast<int>(0x8002003cu);
void Require(bool value, const char* message) {
if (!value) {
std::fprintf(stderr, "%s\n", message);
std::abort();
}
}
std::uint64_t ThreadCycles() {
ULONG64 cycles = 0;
Require(QueryThreadCycleTime(GetCurrentThread(), &cycles) != 0, "QueryThreadCycleTime failed");
return cycles;
}
std::uint64_t BusyCycles(int milliseconds) {
const auto before = ThreadCycles();
const auto until = Clock::now() + std::chrono::milliseconds(milliseconds);
while (Clock::now() < until) YieldProcessor();
return ThreadCycles() - before;
}
struct Handoff {
PthreadMutex mutex = nullptr;
bool timed = false;
bool destroy = false;
std::atomic<bool> waiting{false};
std::atomic<std::int64_t> released{0};
std::int64_t latencyNanos = 0;
std::uint64_t waitCycles = 0;
};
struct HandoffResult {
std::int64_t latencyNanos;
std::uint64_t waitCycles;
};
void* APS5_VABI HandoffWorker(void* opaque) {
auto& context = *static_cast<Handoff*>(opaque);
const auto cyclesBefore = ThreadCycles();
context.waiting.store(true);
Require((context.timed ? scePthreadMutexTimedlock(&context.mutex, 5000000) : scePthreadMutexLock(&context.mutex)) == 0, "the waiter did not acquire the mutex");
const auto acquired = Clock::now().time_since_epoch().count();
context.waitCycles = ThreadCycles() - cyclesBefore;
context.latencyNanos = acquired - context.released.load();
Require(scePthreadMutexUnlock(&context.mutex) == 0, "the waiter could not unlock");
if (context.destroy) Require(scePthreadMutexDestroy(&context.mutex) == 0, "the waiter could not destroy the mutex");
return nullptr;
}
HandoffResult RunHandoff(PthreadMutex mutex, bool timed, bool destroy, int holdMilliseconds, int extraMicroseconds = 0) {
Handoff context;
context.mutex = mutex;
context.timed = timed;
context.destroy = destroy;
PthreadMutex holder = mutex;
Require(scePthreadMutexLock(&holder) == 0, "the holder could not lock");
Pthread worker = nullptr;
Require(scePthreadCreate(&worker, nullptr, HandoffWorker, &context, "mutex waiter") == 0, "cannot create the waiter");
while (!context.waiting.load()) std::this_thread::yield();
std::this_thread::sleep_for(std::chrono::milliseconds(holdMilliseconds));
const auto extra = Clock::now() + std::chrono::microseconds(extraMicroseconds);
while (Clock::now() < extra) YieldProcessor();
context.released.store(Clock::now().time_since_epoch().count());
Require(scePthreadMutexUnlock(&holder) == 0, "the holder could not unlock");
Require(scePthreadJoin(worker, nullptr) == 0, "cannot join the waiter");
return {context.latencyNanos, context.waitCycles};
}
PthreadMutex CreateMutex(bool recursive) {
PthreadMutexattr attributes = nullptr;
Require(scePthreadMutexattrInit(&attributes) == 0 && scePthreadMutexattrSettype(&attributes, recursive ? 2 : 1) == 0, "cannot prepare mutex attributes");
PthreadMutex mutex = nullptr;
Require(scePthreadMutexInit(&mutex, &attributes, nullptr) == 0, "cannot create the mutex");
Require(scePthreadMutexattrDestroy(&attributes) == 0, "cannot destroy mutex attributes");
return mutex;
}
struct Contention {
PthreadMutex mutex = nullptr;
std::uint64_t rounds = 0;
std::atomic<bool> start{false};
std::atomic<bool> stop{false};
std::uint64_t shared = 0;
std::array<std::uint64_t, 4> acquired{};
std::atomic<int> next{0};
};
void* APS5_VABI ContentionWorker(void* opaque) {
auto& context = *static_cast<Contention*>(opaque);
const int index = context.next.fetch_add(1);
PthreadMutex mutex = context.mutex;
while (!context.start.load()) std::this_thread::yield();
std::uint64_t count = 0;
while (context.rounds ? count < context.rounds : !context.stop.load(std::memory_order_relaxed)) {
Require(scePthreadMutexLock(&mutex) == 0, "contended lock failed");
const std::uint64_t seen = context.shared;
for (int spin = 0; spin < 64; ++spin) YieldProcessor();
context.shared = seen + 1;
Require(scePthreadMutexUnlock(&mutex) == 0, "contended unlock failed");
++count;
for (int spin = 0; spin < 64; ++spin) YieldProcessor();
}
context.acquired[index] = count;
return nullptr;
}
void* APS5_VABI TimeoutWorker(void* opaque) {
PthreadMutex mutex = *static_cast<PthreadMutex*>(opaque);
const auto started = Clock::now();
Require(scePthreadMutexTimedlock(&mutex, 20000) == TimedOut, "a timed lock of a held mutex did not time out");
Require(Clock::now() - started >= std::chrono::milliseconds(20), "a timed lock of a held mutex timed out before its deadline");
return nullptr;
}
}
int main(int argc, char** argv) {
const bool report = argc > 1 && std::strcmp(argv[1], "--report") == 0;
const std::uint64_t busyCycles = report ? BusyCycles(20) * 10 : 0;
for (const bool recursive : {false, true}) {
PthreadMutex mutex = CreateMutex(recursive);
for (const bool timed : {false, true}) {
if (!report) {
for (int round = 0; round < 4; ++round) RunHandoff(mutex, timed, false, 3);
continue;
}
std::vector<std::int64_t> latencies;
for (int round = 0; round < 31; ++round) latencies.push_back(RunHandoff(mutex, timed, false, 3, round * 617 % 1000).latencyNanos);
std::sort(latencies.begin(), latencies.end());
const auto blocked = RunHandoff(mutex, timed, false, 200);
const double share = 100.0 * static_cast<double>(blocked.waitCycles) / static_cast<double>(busyCycles);
std::printf("%s %s: handoff median %.1f us, p90 %.1f us, max %.1f us; a waiter blocked for 200 ms used %.1f %% of one processor\n",
recursive ? "recursive" : "errorcheck", timed ? "timed lock" : "lock",
latencies[15] / 1000.0, latencies[27] / 1000.0, latencies[30] / 1000.0, share);
}
PthreadMutex held = mutex;
Require(scePthreadMutexLock(&held) == 0, "cannot hold the mutex for the timeout");
Pthread timeout = nullptr;
Require(scePthreadCreate(&timeout, nullptr, TimeoutWorker, &mutex, "mutex timeout") == 0, "cannot create the timeout waiter");
Require(scePthreadJoin(timeout, nullptr) == 0, "cannot join the timeout waiter");
Require(scePthreadMutexUnlock(&held) == 0, "cannot release the mutex after the timeout");
Contention context;
context.mutex = mutex;
context.rounds = report ? 0 : 20000;
std::array<Pthread, 4> workers{};
for (auto& worker : workers) Require(scePthreadCreate(&worker, nullptr, ContentionWorker, &context, "mutex contender") == 0, "cannot create a contender");
const auto started = Clock::now();
context.start.store(true);
if (report) {
std::this_thread::sleep_for(std::chrono::milliseconds(300));
context.stop.store(true);
}
for (auto& worker : workers) Require(scePthreadJoin(worker, nullptr) == 0, "cannot join a contender");
const double seconds = std::chrono::duration<double>(Clock::now() - started).count();
std::uint64_t total = 0;
for (const auto count : context.acquired) total += count;
Require(total == context.shared, "the mutex did not exclude its contenders");
const auto fewest = *std::min_element(context.acquired.begin(), context.acquired.end());
if (report) {
std::printf("%s: four contenders made %.0f acquisitions per second, the slowest %.0f per second\n",
recursive ? "recursive" : "errorcheck", total / seconds, fewest / seconds);
} else {
Require(fewest == context.rounds, "a contender did not finish its acquisitions");
}
Require(scePthreadMutexDestroy(&mutex) == 0, "cannot destroy the mutex");
for (int round = 0; round < 200; ++round) RunHandoff(CreateMutex(recursive), round % 2 != 0, true, 0);
}
std::puts("guest mutex contention: PASS");
return 0;
}