AuroraRuntime/Source/Threading/Threads/AuOSThread.cpp

1616 lines
43 KiB
C++

/***
Copyright (C) 2021 J Reece Wilson (a/k/a "Reece"). All rights reserved.
File: AuOSThread.cpp
Date: 2021-6-12
Author: Reece
***/
#include <Source/RuntimeInternal.hpp>
#include "AuThreads.hpp"
#include <Source/Threading/AuWaitFor.hpp>
#include "AuOSThread.hpp"
#include "AuThreadHandles.hpp"
#include "TLSView.hpp"
#if defined(AURORA_IS_LINUX_DERIVED)
#include <sys/resource.h>
#include <cxxabi.h>
#endif
#if defined(AURORA_HAS_PTHREADS)
#include <sched.h>
#endif
#if defined(AURORA_PLATFORM_WIN32)
#include <Aux_ulib.h>
#endif
#if defined(AURORA_IS_MODERNNT_DERIVED)
static auto const kThreadPowerThrottling = Aurora::THREAD_INFORMATION_CLASS::ThreadPowerThrottling;
struct THREAD_POWER_THROTTLING_STATE2
{
ULONG Version;
ULONG ControlMask;
ULONG StateMask;
};
#endif
#if defined(AURORA_COMPILER_CLANG)
// warning: enumeration values 'kEnumCount' and 'kEnumInvalid' not handled in switch [-Wswitch
#pragma clang diagnostic ignored "-Wswitch"
// Yea, I don't give a shit.
#endif
#include "AuSpawnThread.hpp"
namespace Aurora
{
void RuntimeLateClean();
}
namespace Aurora::Threading::Threads
{
static struct DtorFlipper
{
DtorFlipper() : bActive(true)
{
}
~DtorFlipper()
{
bActive = false;
}
bool bActive;
} gGlobal;
static ThreadInfo gDummyThreadInfo;
#if defined(AURORA_IS_POSIX_DERIVED)
static void HandleSigAbortThread(int a)
{
((OSThread *)HandleCurrent())->InternalKillForceNtfy();
SysPanic("Couldn't terminate thread context");
}
#endif
OSThread::OSThread(const ThreadInfo &info) : info_(info),
epExecEvent(false, false, true)
{
this->name_ = info.name.value_or("Aurora Thread");
if (info.name)
{
this->uNameIdentity_ = AuFnv1a32Runtime(this->name_.data(), this->name_.size());
}
// maybe we should atomic exchange compare these when needed frogthink
this->terminatedSignalLs_ = AuLoop::NewLSEvent(true, false, true);
this->terminateSignalLs_ = AuLoop::NewLSEvent(true, false, true);
this->terminated_ = AuThreadPrimitives::EventShared(true, false, true);
this->terminateSignal_ = AuThreadPrimitives::EventShared(true, false, true);
SysAssert(this->terminatedSignalLs_ && this->terminateSignalLs_ && this->terminated_ && this->terminateSignal_);
this->InitThreadCreateTime();
}
OSThread::OSThread() : info_(gDummyThreadInfo),
epExecEvent(false, false, true)
{
this->name_ = "Main Thread";
this->terminated_ = AuThreadPrimitives::EventShared(true, false, true);
this->terminateSignal_ = AuThreadPrimitives::EventShared(true, false, true);
SysAssert(this->terminated_ && this->terminateSignal_);
this->bNotOwned = true;
this->InitThreadCreateTime();
}
OSThread::OSThread(AuUInt64 id) : info_(gDummyThreadInfo),
epExecEvent(false, false, true)
{
this->name_ = "System Thread";
this->handle_ = reinterpret_cast<decltype(handle_)>(id);
this->bNotOwned = true;
this->uNameIdentity_ = AuFnv1a32("System Thread");
}
bool DeadTest();
OSThread::~OSThread()
{
bool bDetached {};
bool bDetachedSuccess {};
if (this->bNotOwned)
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
auto hHandle = (HANDLE)this->handle_;
AuWin32CloseHandle(hHandle);
#endif
return;
}
if (!gGlobal.bActive)
{
return;
}
#if 0
if (DeadTest())
{
return;
}
#endif
if (this->contextUsed_)
{
this->epExecEvent->Wait();
{
AU_LOCK_GUARD(this->pFlag->mutex);
this->pFlag->bLock = true;
}
if (this->detached_)
{
bDetached = true;
}
else
{
if (gRuntimeRunLevel <= 3)
{
Terminate();
}
else if (gRuntimeRunLevel >= 5)
{
// Application is dead
}
else
{
if (this->terminated_)
{
this->exiting_ = true;
if (this->terminated_->LockMS(5'000))
{
return;
}
}
// Kill the current OS thread instance
TeminateOSContext(false);
this->exitOnlyOnce_->Unlock();
#if defined(AURORA_IS_POSIX_DERIVED)
if (this->terminated_)
{
this->terminated_->Lock();
}
#endif
}
}
}
else
{
if (auto pTerminated = AuExchange(this->terminatedSignalLs_, {}))
{
pTerminated->Set();
}
if (auto pTerminated = AuExchange(this->terminated_, {}))
{
pTerminated->Set();
}
}
FreeOSContext();
if (bDetached)
{
if (this->tlsReferenceThread_)
{
AU_LOCK_GUARD(this->tlsLock_);
AU_LOCK_GUARD(this->tlsReferenceThread_->tlsLock_);
SetThreadKey(this->tlsReferenceThread_->tls_);
AuExchange(this->tlsReferenceThread_->tls_, this->tls_);
AuExchange(this->tlsReferenceThread_->threadFeatures_, this->threadFeatures_);
bDetachedSuccess = true;
}
}
if (bDetachedSuccess || !bDetached)
{
HookReleaseThreadResources();
}
}
// Called whenever an aurora thread is exiting
void OSThread::HookOnExit()
{
}
// Called whenever a thread object is released, or on thread termination
// Whichever is soonest
// It is possible a user may wish to keep a thread handle open
// Is is expected for a system thread to release on dtor unlike our aurora threads
void OSThread::HookReleaseThreadResources()
{
AU_LOCK_GUARD(this->tlsLock_);
for (const auto &feature : this->threadFeatures_)
{
feature->Cleanup();
}
}
AuUInt64 OSThread::SetNoUnwindTerminateExitWatchDogTimeoutInMS(AuUInt64 uMS)
{
if (!uMS)
{
uMS = AuSToMS<AuUInt64>(15);
}
qwExitTimeout = uMS;
return qwExitTimeout;
}
void OSThread::AddLastHopeTlsHook(const AuSPtr<AuThreads::IThreadFeature> &feature)
{
AU_LOCK_GUARD(this->tlsLock_);
if (!feature)
{
return;
}
AuTryInsert(this->threadFeatures_, feature);
}
void OSThread::Detach()
{
this->detached_ = true;
}
AuSPtr<IWaitable> OSThread::GetShutdownWaitable()
{
if (!this->terminated_)
{
this->terminated_ = AuThreadPrimitives::EventShared(true, false, true);
}
return this->terminated_;
}
void OSThread::SendExitSignal()
{
this->exiting_ = true;
if (this->terminateSignalLs_)
{
this->terminateSignalLs_->Set();
}
if (this->terminateSignal_)
{
this->terminateSignal_->Set();
}
}
bool OSThread::Run()
{
AU_DEBUG_MEMCRUNCH;
if (!this->terminated_)
{
SysPanic("::Run called on system thread");
}
if (AuExchange(this->contextUsed_, true))
{
return false;
}
this->terminated_->Reset();
auto copy = this->info_.callbacks;
return ExecuteNewOSContext([=]()
{
try
{
// this functional backends are being deprecated
if (copy)
{
copy->OnEntry(this);
}
}
catch (...)
{
Debug::PrintError();
}
});
}
void OSThread::Terminate()
{
this->PrivateUserDataClear();
while ((!this->terminated_) || (!this->terminated_->TryLock()))
{
if (Exit(false)) break;
}
}
void OSThread::UnsafeForceTerminateSignal()
{
TeminateOSContext(false);
}
bool OSThread::Exit(bool willReturnToOS, bool isEOL)
{
if (GetThread() == this)
{
if (!this->InternalKill(false))
{
return false;
}
// release handle + sigterm
if (!willReturnToOS)
{
TeminateOSContext(true);
while (true)
{
ContextYield();
}
}
}
else
{
// exit signal
this->exiting_ = true;
if (!this->terminated_)
{
return true;
}
// attempt to join with the thread once it has exited, or timeout
if (this->terminated_->LockMS(this->qwExitTimeout))
{
return true;
}
// Do not force terminate if we're marked as dead and still running
// The thread must've requested suicide and got stuck in a lengthy clean up effort
if (!this->exitOnlyOnce_->TryLock())
{
AuLogWarn("Watchdog error - OS thread context didn't finish in {} MS, but he should exiting now.", this->qwExitTimeout);
return false;
}
AuLogWarn("Watchdog error - OS thread context didn't finish in {} MS, forcefully terminating without a watchdog overlooking onExit", this->qwExitTimeout);
// Kill the current OS thread instance
TeminateOSContext(false);
// Dispatch kill callback from within an emulated thread context (no switch, just just change thread tls)
ExecuteInDeadThread([=]()
{
this->InternalKill(true);
});
this->exitOnlyOnce_->Unlock();
}
return true;
}
bool OSThread::Exiting()
{
return this->exiting_;
}
void OSThread::SetName(const AuString &name)
{
AU_LOCK_GUARD(this->metaMutex);
if (gRuntimeConfig.threadingConfig.bNoThreadNames)
{
return;
}
this->name_ = name;
this->uNameIdentity_ = AuFnv1a32Runtime(name.data(), name.size());
this->UpdateName();
}
void OSThread::SetNameIdentity(AuUInt32 uNameIdentity)
{
this->uNameIdentity_ = uNameIdentity;
}
EThreadThrottle OSThread::GetThrottle()
{
return this->throttle_;
}
EThreadPriority OSThread::GetPriority()
{
return this->prio_;
}
AuHwInfo::CpuBitId OSThread::GetAffinityMask()
{
return this->mask_;
}
AuString OSThread::GetName()
{
return this->name_;
}
AuUInt32 OSThread::GetNameIdentity()
{
return this->uNameIdentity_;
}
void OSThread::SetAffinityMask(const HWInfo::CpuBitId &mask)
{
auto zero = HWInfo::CpuBitId();
if (mask == zero ||
mask == zero.Not())
{
this->mask_ = HWInfo::GetCPUInfo().maskAllCores;
this->userManagingAffinity_ = false;
UpdatePrio(this->throttle_, this->prio_);
}
else
{
this->mask_ = mask;
this->userManagingAffinity_ = true;
}
UpdateAffinity(this->mask_);
AffinityPrioThrottleTickAmendECores();
}
void OSThread::SetPriority(EThreadPriority prio)
{
if (!EThreadPriorityIsValid(prio))
{
return;
}
UpdatePrio(this->throttle_, prio);
AffinityPrioThrottleTickAmendECores();
}
void OSThread::SetThrottle(EThreadThrottle throttle)
{
if (!EThreadThrottleIsValid(throttle))
{
return;
}
UpdatePrio(throttle, this->prio_);
this->throttle_ = throttle;
AffinityPrioThrottleTickAmendECores();
}
bool OSThread::ExecuteNewOSContext(AuFunction<void()> task)
{
this->task_ = task;
this->handle_ = 0;
auto ret = SpawnThread([this]()
{
while (!this->HasValidThreadHandleYet())
{
AuThreading::ContextYield();
}
if (!this->tls_)
{
this->tls_ = GetThreadKey();
}
this->_ThreadEP();
}, GetName(), this->info_.stackSize);
if (ret.first)
{
this->handle_ = (decltype(handle_))ret.second;
}
return ret.first;
}
void OSThread::AffinityPrioThrottleTickAmendECores()
{
auto zero = HWInfo::CpuBitId();
auto &cpuInfo = AuHwInfo::GetCPUInfo();
auto sysEcores = cpuInfo.maskECores;
bool bIsMaskProbablyDefault = this->mask_ == zero || this->mask_ == zero.Not();
bool bSystemHasNoECores = sysEcores == zero;
EThreadPriority prio = this->prio_;
if (this->throttle_ == EThreadThrottle::eEfficient &&
bSystemHasNoECores)
{
if (bIsMaskProbablyDefault)
{
sysEcores = cpuInfo.maskAllCores;
}
/**
* We have a lot of code that will specify an efficiency throttle and then
* arbitrarily use prio levels to (hopefully) order work in the os scheduler.
* Systems without ecores would see a higher prio thread than normal tasks, if
* we do nothing about this condition. On systems without ecores, the efficiency
* throttle should just lock the prio to low, instead of attempting to use prio
* as a localized ecore scheduler hint.
*/
prio = EThreadPriority::ePrioLow;
UpdatePrio(this->throttle_, prio);
UpdateAffinity(sysEcores);
}
}
void AttachSignalKiller();
void OSThread::InitThreadCreateTime()
{
uClockCreationTime[0] = AuTime::CurrentClockNS();
uClockCreationTime[1] = AuTime::SteadyClockNS();
uClockCreationTime[2] = AuTime::ProcessClockNS();
}
AuUInt64 OSThread::GetThreadCreationTime(Time::EClock eClock)
{
if (!AuTime::EClockIsValid(eClock))
{
SysPushErrorArg();
return {};
}
switch (eClock)
{
case Time::EClock::eWall:
return uClockCreationTime[0];
case Time::EClock::eSteady:
return uClockCreationTime[1];
case Time::EClock::eProcessTime:
return uClockCreationTime[2];
default:
SysPushErrorArg();
return {};
}
}
void OSThread::_ThreadEP()
{
// Poke TLS reference thread entity
// TODO: we need an internal OSThread *TryPokeTLSThread()
auto osThread = static_cast<OSThread *>(GetThread());
this->tlsReferenceThread_ = osThread;
bool bFailing {};
OSAttach();
#if defined(AURORA_IS_POSIX_DERIVED)
this->bLongJmpOnce = false;
if (setjmp(env) != 0)
{
Exit(true);
return;
}
this->bSupportsAltKill = true;
#endif
auto pFlag = this->pFlag;
auto pA = this->terminatedSignalLs_;
auto pB = this->terminated_;
this->InitThreadCreateTime();
try
{
AU_DEBUG_MEMCRUNCH;
if (auto task = task_)
{
this->epExecEvent->Set();
AU_DEBUG_REVERSE_MEMCRUNCH;
task();
}
}
#if !defined(AURORA_IS_POSIX_DERIVED)
catch (...)
{
SysPushErrorHAL("OS Thread Aborted");
}
AU_LOCK_GUARD(pFlag->mutex);
if (pFlag->bLock)
{
if (pA)
{
pA->Set();
}
if (pB)
{
pB->Set();
}
return;
}
this->SignalDeath();
Exit(true);
#else
#if defined(AURORA_COMPILER_GCC)
catch (abi::__forced_unwind&)
{
throw;
}
#endif
catch (...)
{
AU_LOCK_GUARD(pFlag->mutex);
if (pFlag->bLock)
{
return;
}
bFailing = true;
if (!Aurora::kIsDebugBuild)
{
SysPushErrorHAL("OS Thread Aborted");
}
// "Safer" update
this->HookOnExit();
this->SignalDeath();
}
AU_LOCK_GUARD(pFlag->mutex);
if (pFlag->bLock)
{
if (pA)
{
pA->Set();
}
if (pB)
{
pB->Set();
}
return;
}
if (!bFailing)
{
Exit(true);
}
#endif
}
void OSThread::ExecuteInDeadThread(AuFunction<void()> callback)
{
auto old = HandleCurrent();
if (this == old) [[unlikely]]
{
callback();
return;
}
if (old)
{
static_cast<OSThread *>(old)->OSDeatach();
}
auto uOldHandle = GetThreadKey();
this->OSAttach();
callback();
if (old)
{
HandleRegister(old);
static_cast<OSThread *>(old)->OSDeatach();
}
else [[unlikely]]
{
HandleRemove();
}
SetThreadKey(uOldHandle);
}
void OSThread::UpdateName()
{
AU_LOCK_GUARD(this->metaMutex);
if (!this->HasValidThreadIdYet())
{
return;
}
if (gRuntimeConfig.threadingConfig.bNoThreadNames)
{
return;
}
#if defined(AURORA_IS_MODERNNT_DERIVED)
if (this->handle_ == INVALID_HANDLE_VALUE)
{
return;
}
if ((AuBuild::kCurrentPlatform == AuBuild::EPlatform::ePlatformWin32) && (!AuSwInfo::IsWindows10OrGreater()))
{
static const DWORD kMSVCExceptionSetName = 0x406D1388;
#pragma pack(push,8)
typedef struct tagTHREADNAME_INFO
{
DWORD dwType;
LPCSTR szName;
DWORD dwThreadID;
DWORD dwFlags;
} THREADNAME_INFO;
#pragma pack(pop)
if (!IsDebuggerPresent())
{
return;
}
THREADNAME_INFO info;
info.dwType = 0x1000;
info.szName = this->name_.c_str();
if (pGetThreadId)
{
info.dwThreadID = pGetThreadId(this->handle_);
}
else if (this->optUnixThreadId_.ValueOr(0) == GetCurrentThreadId())
{
info.dwThreadID = this->optUnixThreadId_.ValueOr(0);
}
else
{
return;
}
info.dwFlags = 0;
auto raise = AuStaticCast<void(__cdecl *)(THREADNAME_INFO &)>([](THREADNAME_INFO &info)
{
__try
{
RaiseException(kMSVCExceptionSetName, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR *)&info);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
}
});
raise(info);
}
else
{
if (pSetThreadDescription)
{
pSetThreadDescription(this->handle_, AuLocale::ConvertFromUTF8(this->name_).c_str());
}
}
#elif defined(AURORA_HAS_PTHREADS)
if (!this->handle_)
{
return;
}
pthread_setname_np(this->handle_, this->name_.c_str());
#endif
}
static void AttachSignalKiller()
{
#if defined(AURORA_IS_POSIX_DERIVED)
struct sigaction action =
{
.sa_handler = HandleSigAbortThread,
.sa_flags = 0
};
::sigemptyset(&action.sa_mask);
::sigaction(gRuntimeConfig.linuxConfig.uSignalTerminate, &action, nullptr);
#endif
}
void OSThread::MakeMain()
{
this->bNotOwned = true;
this->name_ = "Main System Thread";
this->uNameIdentity_ = AuFnv1a32("Main System Thread");
this->OSAttach();
}
void OSThread::OSAttach()
{
AU_DEBUG_MEMCRUNCH;
HandleRegister(this);
{
AU_LOCK_GUARD(this->metaMutex);
#if defined(AURORA_IS_LINUX_DERIVED)
this->optUnixThreadId_ = gettid();
#elif defined(AURORA_IS_XNU_DERIVED)
this->optUnixThreadId_ = pthread_getthreadid_np();
#elif defined(AURORA_IS_BSD_DERIVED)
#if __FreeBSD_version < 900031
long lwpid;
thr_self(&lwpid);
this->optUnixThreadId_ = lwpid;
#elif __FreeBSD_version > 900030
this->optUnixThreadId_ = pthread_getthreadid_np();
#else
static_assert(false);
#endif
#elif defined(AURORA_HAS_PTHREADS)
this->optUnixThreadId_ = 0; // !!!!
#endif
#if defined(AURORA_PLATFORM_WIN32)
this->optUnixThreadId_ = GetCurrentThreadId();
#endif
}
if (this->tls_)
{
SetThreadKey(this->tls_);
}
{
AU_LOCK_GUARD(this->metaMutex);
UpdatePrio(this->throttle_, this->prio_);
SetAffinityMask(this->mask_);
AffinityPrioThrottleTickAmendECores();
UpdateName();
}
}
static AuHashMap<EThreadPriority, int> kNiceMap
{
{
EThreadPriority::ePrioRT, -19
},
{
EThreadPriority::ePrioAboveHigh, -15
},
{
EThreadPriority::ePrioHigh, -11
},
{
EThreadPriority::ePrioNormal, -2
},
{
EThreadPriority::ePrioLow, 5
},
{
EThreadPriority::ePrioLowest, 15
}
};
#if defined(AURORA_IS_MODERNNT_DERIVED)
static const AuHashMap<EThreadPriority, int> kWin32Map
{
{
EThreadPriority::ePrioRT, THREAD_PRIORITY_TIME_CRITICAL
},
{
EThreadPriority::ePrioAboveHigh, THREAD_PRIORITY_HIGHEST
},
{
EThreadPriority::ePrioHigh, THREAD_PRIORITY_ABOVE_NORMAL
},
{
EThreadPriority::ePrioNormal, THREAD_PRIORITY_NORMAL
},
{
EThreadPriority::ePrioLow, THREAD_PRIORITY_BELOW_NORMAL
},
{
EThreadPriority::ePrioLowest, THREAD_PRIORITY_LOWEST
}
};
#endif
#if defined(AURORA_IS_XNU_DERIVED)
static const AuHashMap<AuPair<EThreadThrottle, EThreadPriority>, AuPair<AuUInt32, int>> kXnuQoSLevel
{
// Rt
{
AuMakePair(EThreadThrottle::eNormal, EThreadPriority::ePrioRT), AuMakePair(DISPATCH_QUEUE_PRIORITY_HIGH, 10)
},
{
AuMakePair(EThreadThrottle::eNormal, EThreadPriority::ePrioAboveHigh), AuMakePair(DISPATCH_QUEUE_PRIORITY_DEFAULT, 5)
},
{
AuMakePair(EThreadThrottle::eNormal, EThreadPriority::ePrioHigh), AuMakePair(DISPATCH_QUEUE_PRIORITY_DEFAULT, 1)
},
{
AuMakePair(EThreadThrottle::eNormal, EThreadPriority::ePrioNormal), AuMakePair(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
},
{
AuMakePair(EThreadThrottle::eNormal, EThreadPriority::ePrioLow), AuMakePair(DISPATCH_QUEUE_PRIORITY_LOW, 0)
},
{
AuMakePair(EThreadThrottle::eNormal, EThreadPriority::ePrioLowest), AuMakePair(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)
},
// Perf
{
AuMakePair(EThreadThrottle::ePerformance, EThreadPriority::ePrioRT), AuMakePair(DISPATCH_QUEUE_PRIORITY_HIGH, 10)
},
{
AuMakePair(EThreadThrottle::ePerformance, EThreadPriority::ePrioAboveHigh), AuMakePair(DISPATCH_QUEUE_PRIORITY_HIGH, 8)
},
{
AuMakePair(EThreadThrottle::ePerformance, EThreadPriority::ePrioHigh), AuMakePair(DISPATCH_QUEUE_PRIORITY_HIGH, 3)
},
{
AuMakePair(EThreadThrottle::ePerformance, EThreadPriority::ePrioNormal), AuMakePair(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
},
{
AuMakePair(EThreadThrottle::ePerformance, EThreadPriority::ePrioLow), AuMakePair(DISPATCH_QUEUE_PRIORITY_DEFAULT, 1)
},
{
AuMakePair(EThreadThrottle::ePerformance, EThreadPriority::ePrioLowest), AuMakePair(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
},
// Efficient
{
AuMakePair(EThreadThrottle::eEfficient, EThreadPriority::ePrioRT), AuMakePair(DISPATCH_QUEUE_PRIORITY_LOW, 10)
},
{
AuMakePair(EThreadThrottle::eEfficient, EThreadPriority::ePrioAboveHigh), AuMakePair(DISPATCH_QUEUE_PRIORITY_LOW, 1)
},
{
AuMakePair(EThreadThrottle::eEfficient, EThreadPriority::ePrioHigh), AuMakePair(DISPATCH_QUEUE_PRIORITY_LOW, 0)
},
{
AuMakePair(EThreadThrottle::eEfficient, EThreadPriority::ePrioNormal), AuMakePair(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)
},
{
AuMakePair(EThreadThrottle::eEfficient, EThreadPriority::ePrioLow), AuMakePair(DISPATCH_QUEUE_PRIORITY_BACKGROUND, -1)
},
{
AuMakePair(EThreadThrottle::eEfficient, EThreadPriority::ePrioLowest), AuMakePair(DISPATCH_QUEUE_PRIORITY_BACKGROUND, -2)
}
};
#endif
bool OSThread::HasValidThreadIdYet()
{
// set by the new thread context on attachment
return bool(this->optUnixThreadId_);
}
bool OSThread::HasValidThreadHandleYet()
{
// set by thread spawner
return this->handle_ &&
#if defined(INVALID_HANDLE_VALUE)
!(this->handle_ == INVALID_HANDLE_VALUE)
#else
true
#endif
;
}
#if defined(AURORA_IS_LINUX_DERIVED)
static void DoWarnBadOperatingSystem()
{
static AuInitOnce gInitOnce;
gInitOnce.Call([]()
{
AuLogWarn("Cannot update real time thread priority. This is most likely due to the POSIX ecosystem consisting of toy academic kernels/systems with incompetent maintainers.");
AuLogWarn("We will not use RT-Kit to keep your Linux machine alive through XDG_FREEEEeeEEEeEeEEeeeDESKTOP_HOPES_AND_DREAMS, dbus-slop, and systemd-specific daemons.");
AuLogWarn("Obligatory sod off System-D, IBM Red Hat, and Lennart Poettering 🤮");
AuLogWarn("To reiterate, if this isn't clear enough already, we will not bloat our toolchains, monorepos, and binary releases over not portable and useless systemd dbus artefacts.");
AuLogWarn("Hint: try (1) : append:\n"
"* hard rtprio unlimited\n"
"* soft rtprio unlimited\n"
"* hard priority unlimited\n"
"* soft priority unlimited\n"
"... to /etc/security/limits.conf");
AuLogWarn("Hint: try (2) : in bash, elevate using: '[doas/sudo] prlimit --rtprio=unlimited --pid=$$' before running the main executable");
AuLogWarn("Hint: try (3) : check /etc/systemd/[system/user].conf");
});
}
#endif
void OSThread::UpdatePrio(EThreadThrottle throttle, EThreadPriority prio)
{
AU_LOCK_GUARD(this->metaMutex);
if (!this->HasValidThreadIdYet())
{
this->prio_ = prio;
this->throttle_ = throttle;
return;
}
#if defined(AURORA_IS_MODERNNT_DERIVED)
if (this->handle_ == INVALID_HANDLE_VALUE)
{
return;
}
const int *val;
if (!AuTryFind(kWin32Map, prio, val))
{
return;
}
if (!SetThreadPriority(this->handle_, *val))
{
SysPushErrorHAL("Couldn't update thread priority");
}
THREAD_POWER_THROTTLING_STATE2 throttlingState {};
throttlingState.Version = THREAD_POWER_THROTTLING_CURRENT_VERSION;
switch (throttle)
{
case EThreadThrottle::eNormal:
throttlingState.ControlMask = 0;
throttlingState.StateMask = 0;
break;
case EThreadThrottle::ePerformance:
throttlingState.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED;
throttlingState.StateMask = 0;
break;
case EThreadThrottle::eEfficient:
throttlingState.ControlMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED;
throttlingState.StateMask = THREAD_POWER_THROTTLING_EXECUTION_SPEED;
break;
}
if (pSetThreadInformation)
{
pSetThreadInformation(this->handle_,
kThreadPowerThrottling,
&throttlingState,
sizeof(throttlingState));
}
//#elif defined(AURORA_IS_XNU_DERIVED)
#elif defined(AURORA_HAS_PTHREADS)
auto unixThreadId = this->optUnixThreadId_.Value();
sched_param param {};
int policyNonRT {};
int schedA {};
int scheduler {};
#if defined(AURORA_IS_LINUX_DERIVED)
rlimit rl;
#endif
#if defined(AURORA_IS_POSIX_DERIVED)
static const auto GetPrioLevel = [](int sched, EThreadPriority prio)
{
const int *val;
if (!AuTryFind(kNiceMap, prio, val))
{
return (int)sched_get_priority_max(sched);
}
int min = sched_get_priority_min(sched);
int max = sched_get_priority_max(sched);
float f = ((1.0 - ((float(*val) + 19.0) / (19.0 + 20.0))) * float(max - min)) + float(min);
return (int)f;
};
#endif
// if switch to rtprio scheduling
if (prio == EThreadPriority::ePrioRT)
{
/////////////////////////////////
#if defined(AURORA_IS_POSIX_DERIVED)
#if defined(SCHED_RR)
param.sched_priority = sched_get_priority_max((schedA = SCHED_RR));
#else
param.sched_priority = sched_get_priority_max((schedA = SCHED_FIFO));
#endif
if (::sched_setscheduler(unixThreadId, schedA, &param) == 0)
{
goto gtfo;
}
{
// if switch to rtprio with linux scheduling and rlimit adjustments
#if defined(AURORA_IS_LINUX_DERIVED)
////////////////////////////////////////////////////////////////////
if (getrlimit(RLIMIT_RTPRIO, &rl) == 0)
{
rl.rlim_max = param.sched_priority;
if (rl.rlim_max != RLIM_INFINITY)
{
DoWarnBadOperatingSystem();
rl.rlim_cur = rl.rlim_max;
}
else
{
rl.rlim_cur = rl.rlim_max;
}
param.sched_priority = rl.rlim_cur;
#if defined(SCHED_RR)
if (::sched_setscheduler(unixThreadId, SCHED_RR, &param) == 0)
{
goto gtfo;
}
#endif
if (::setrlimit(RLIMIT_RTPRIO, &rl) != 0)
{
DoWarnBadOperatingSystem();
}
#if defined(SCHED_RR)
if (::sched_setscheduler(unixThreadId, SCHED_RR, &param) == 0)
{
goto gtfo;
}
#endif
param.sched_priority = sched_get_priority_max(SCHED_FIFO);
if (::sched_setscheduler(unixThreadId, SCHED_FIFO, &param) == 0)
{
goto gtfo;
}
}
// else if switch to posix RTPRIO with posix SCHED_FIFO + sched_setscheduler
else
#endif
{
////////////////////////////////////////////////////////////////////////////
param.sched_priority = sched_get_priority_max(SCHED_FIFO);
if (::sched_setscheduler(unixThreadId, SCHED_FIFO, &param) == 0)
{
goto gtfo;
}
}
}
// !defined(AURORA_IS_POSIX_DERIVED) with pthreads
#else
#if defined(SCHED_RR)
param.sched_priority = sched_get_priority_min(SCHED_RR);
// set SCHED_RR with pthreads, if available
if (pthread_setschedparam(this->handle_, SCHED_RR, &param) == 0)
{
goto gtfo;
}
#endif
param.sched_priority = GetPrioLevel(SCHED_FIFO, prio);
// set SCHED_FIFO to the highest prio level with pthreads, if available
if (pthread_setschedparam(this->handle_, SCHED_FIFO, &param) == 0)
{
goto gtfo;
}
#endif
// fall through on error
}
// if linux and the next throttle type requires shifting down to SCHED_IDLE scheduling
#if defined(AURORA_IS_LINUX_DERIVED)
else if (throttle == EThreadThrottle::eEfficient)
{
///////////////////////////////////////////////////////////////////////////////////////
param.sched_priority = GetPrioLevel(SCHED_IDLE, prio);
if (::sched_setscheduler(unixThreadId, SCHED_IDLE, &param) == 0)
{
goto gtfo;
}
// fail
return;
}
// else if restore to regular scheduling condition
else if (this->throttle_ == EThreadThrottle::eEfficient ||
this->prio_ == EThreadPriority::ePrioRT)
#else
else if (this->prio_ == EThreadPriority::ePrioRT)
#endif
//////////////////////////////////////////////////
{
#if defined(AURORA_IS_POSIX_DERIVED)
policyNonRT =
#if defined(AURORA_IS_XNU_DERIVED)
SCHED_FIFO;
#else
SCHED_OTHER;
#endif
param.sched_priority = GetPrioLevel(policyNonRT, prio);
if (::sched_setscheduler(unixThreadId, policyNonRT, &param) == 0)
{
goto gtfo;
}
return;
#endif
}
{
scheduler = ::sched_getscheduler(unixThreadId);
param.sched_priority = GetPrioLevel(scheduler, prio);
if (::sched_setscheduler(unixThreadId, scheduler, &param) == 0)
{
goto gtfo;
}
}
// TODO: We used to set the legacy unix niceness value here.
AuLogWarn("Couldn't set affinity");
#endif
// epilogue
gtfo:
#if defined(AURORA_IS_LINUX_DERIVED) || defined(AURORA_IS_BSD_DERIVED)
switch (throttle)
{
case EThreadThrottle::eNormal:
this->throttleMask_ = AuHwInfo::GetCPUInfo().maskAllCores;
break;
case EThreadThrottle::ePerformance:
this->throttleMask_ = AuHwInfo::GetCPUInfo().maskPCores;
break;
case EThreadThrottle::eEfficient:
this->throttleMask_ = AuHwInfo::GetCPUInfo().maskECores;
break;
}
UpdateAffinity(this->mask_);
#endif
this->prio_ = prio;
}
void OSThread::UpdateAffinity(const HWInfo::CpuBitId &mask)
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
if (this->handle_ == INVALID_HANDLE_VALUE)
{
return;
}
if ((AuBuild::kCurrentPlatform != AuBuild::EPlatform::ePlatformWin32) ||
(AuSwInfo::IsWindows10OrGreater()))
{
auto sets = mask.CpuBitCount() ?
mask.ToCpuSets() :
AuHwInfo::GetCPUInfo().maskAllCores.ToCpuSets();
if (pSetThreadSelectedCpuSets)
{
if (pSetThreadSelectedCpuSets(this->handle_, sets.data(), sets.size()))
{
return;
}
SysPushErrorUnavailableError("SetThreadSelectedCpuSets is expected on modern NT (CoreOS?) excluding Windows; or Win10+");
}
}
#if defined(AURORA_PLATFORM_WIN32)
if (pSetThreadGroupAffinity)
{
GROUP_AFFINITY affinityGroup { 0 };
mask.ToMsWin7GroupAffinity(&affinityGroup);
if (pSetThreadGroupAffinity(this->handle_, &affinityGroup, nullptr))
{
return;
}
}
if (mask.CpuBitCount() <= 32)
{
DWORD_PTR uProcMask {};
DWORD_PTR uSysMask {};
AuUInt32 uProcmask { AuUInt32(mask.lower) };
if (::GetProcessAffinityMask(GetCurrentProcess(), &uProcMask, &uSysMask))
{
uProcmask &= uProcMask;
}
if (::SetThreadAffinityMask(this->handle_, uProcmask))
{
return;
}
}
#endif
SysPushErrorUnavailableError("Couldn't set thread affinity");
return;
#elif defined(AURORA_HAS_PTHREADS)
if (!this->handle_)
{
return;
}
auto mask2 = mask.And(this->throttleMask_);
if (mask2.CpuBitCount() == 0)
{
switch (this->throttle_)
{
case EThreadThrottle::eNormal:
mask2 = AuHwInfo::GetCPUInfo().maskAllCores;
break;
case EThreadThrottle::ePerformance:
mask2 = AuHwInfo::GetCPUInfo().maskPCores;
break;
case EThreadThrottle::eEfficient:
mask2 = AuHwInfo::GetCPUInfo().maskECores;
break;
}
}
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
AuUInt8 index {};
while (mask2.CpuBitScanForward(index, index))
{
CPU_SET(index, &cpuset);
index++;
}
if (!CPU_COUNT(&cpuset))
{
return;
}
if (pthread_setaffinity_np(this->handle_, sizeof(cpuset), &cpuset) != 0)
{
SysPushErrorHAL("Couldn't set affinity mask");
}
return;
#endif
}
void OSThread::OSDeatach()
{
#if defined(AURORA_IS_POSIX_DERIVED)
this->bSupportsAltKill = false;
//struct sigaction action =
//{
// .sa_handler = SIG_DFL
//};
//::sigemptyset(&action.sa_mask);
//::sigaction(gRuntimeConfig.linuxConfig.uSignalTerminate, &action, nullptr);
#endif
}
bool OSThread::InternalKill(bool locked)
{
this->PrivateUserDataClear();
if (this->terminated_)
{
if (!locked)
{
if (!this->exitOnlyOnce_->TryLock())
{
return false;
}
}
}
HookOnExit();
try
{
// dispatch kill callback
if (this->info_.callbacks)
{
this->info_.callbacks->OnExit(this);
}
}
catch (...)
{
Debug::PrintError();
AuLogWarn("Couldn't deinitialize thread");
AuLogWarn("The smart thing to do at this point would be to panic");
AuLogWarn("...but we could continue");
AuLogWarn("Carrying on despite the potential for data integrity loss and memory leaks");
Telemetry::Mayday();
}
HookReleaseThreadResources();
this->SignalDeath();
return true;
}
void OSThread::SignalDeath()
{
if (this->terminatedSignalLs_)
{
this->terminatedSignalLs_->Set();
}
if (this->terminated_)
{
this->exitOnlyOnce_->Unlock();
this->terminated_->Set();
}
}
bool OSThread::InternalKillForceNtfy()
{
this->SignalDeath();
// Great C++ ABI guys...
#if defined(AURORA_HAS_PTHREADS)
{
if (gettid() == getpid())
{
AuProcess::Exit(0);
return true;
}
::longjmp(env, 1);
}
::pthread_exit(nullptr);
#endif
return true;
}
void OSThread::TeminateOSContext(bool calledFromThis)
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
if (this->handle_ == INVALID_HANDLE_VALUE)
{
return;
}
if (calledFromThis)
{
::ExitThread(0);
}
else
{
::TerminateThread(this->handle_, 0);
}
#elif defined(AURORA_HAS_PTHREADS)
if (calledFromThis)
{
::longjmp(env, 1);
::pthread_exit(nullptr);
}
else
{
// pthreads is fun. thats not how unix works...
// pthread_kill(this->handle_, SIGKILL);
// remember signal inheritance is a cluster fuck & the tree will be walked
// this is giving me flashbacks to hacking in apcs into the kernel
// gross...
// let's let nptl handle it
//pthread_cancel(this->handle_);
// ...doesnt work with c++ bc catch handlers get in the way. FUCK
if (this->bSupportsAltKill)
{
::pthread_kill(this->handle_, gRuntimeConfig.linuxConfig.uSignalTerminate);
}
else
{
::pthread_cancel(this->handle_);
}
}
#else
SysPanic("Not implemented");
#endif
}
void OSThread::FreeOSContext()
{
#if defined(AURORA_IS_MODERNNT_DERIVED)
AuWin32CloseHandle(this->handle_);
#endif
}
AuSPtr<AuLoop::ILoopSource> OSThread::GetShutdownLoopSource()
{
if (!this->terminatedSignalLs_)
{
this->terminatedSignalLs_ = AuLoop::NewLSEvent(true, false, true);
}
return this->terminatedSignalLs_;
}
AuSPtr<IWaitable> OSThread::GetShutdownSignalWaitable()
{
return this->terminateSignal_;
}
AuSPtr<AuLoop::ILoopSource> OSThread::GetShutdownSignalLoopSource()
{
return this->terminateSignalLs_;
}
void InitThreading()
{
#if defined(AURORA_PLATFORM_WIN32)
AuxUlibInitialize();
#endif
AttachSignalKiller();
if (auto pThread = GetThread())
{
AuStaticCast<OSThread>(pThread)->MakeMain();
}
}
}