Files
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

698 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#if (defined(__AUTORTFM) && __AUTORTFM)
#include "Context.h"
#include "ContextInlines.h"
#include "AutoRTFMMetrics.h"
#include "CallNestInlines.h"
#include "ExternAPI.h"
#include "FunctionMap.h"
#include "ScopedGuard.h"
#include "StackRange.h"
#include "Stats.h"
#include "Transaction.h"
#include "TransactionInlines.h"
#include "Utils.h"
#if AUTORTFM_PLATFORM_WINDOWS
#include "WindowsHeader.h"
#endif
namespace
{
AutoRTFM::FAutoRTFMMetrics GAutoRTFMMetrics;
}
namespace AutoRTFM
{
FContext* FContext::Instance = nullptr;
FContext* FContext::Create()
{
AUTORTFM_ENSURE(Instance == nullptr);
void* Memory = AutoRTFM::Allocate(sizeof(FContext), alignof(FContext));
Instance = new (Memory) FContext();
return Instance;
}
void ResetAutoRTFMMetrics()
{
GAutoRTFMMetrics = FAutoRTFMMetrics{};
}
// get a snapshot of the current internal metrics
FAutoRTFMMetrics GetAutoRTFMMetrics()
{
return GAutoRTFMMetrics;
}
bool FContext::IsTransactional() const
{
return GetStatus() == EContextStatus::OnTrack;
}
bool FContext::IsCommitting() const
{
switch (GetStatus())
{
default:
return false;
case EContextStatus::Committing:
return true;
}
}
bool FContext::IsCommittingOrAborting() const
{
switch (GetStatus())
{
default:
return true;
case EContextStatus::Idle:
case EContextStatus::OnTrack:
return false;
}
}
bool FContext::IsRetrying() const
{
return GetStatus() == EContextStatus::AbortedByCascadingRetry;
}
void FContext::MaterializeDeferredTransactions()
{
uint64_t NumToAllocate = GetNumDeferredTransactions();
NumDeferredTransactions = 0;
for (uint64_t I = 0; I < NumToAllocate; ++I)
{
StartNonDeferredTransaction(EMemoryValidationLevel::Disabled);
}
}
void FContext::StartTransaction(EMemoryValidationLevel MemoryValidationLevel)
{
if (MemoryValidationLevel != EMemoryValidationLevel::Disabled)
{
MaterializeDeferredTransactions();
StartNonDeferredTransaction(MemoryValidationLevel);
return;
}
NumDeferredTransactions += 1;
AUTORTFM_ENSURE_MSG(CurrentTransaction, "FContext::StartTransaction() can only be called within a scoped transaction");
AUTORTFM_ASSERT(Status == EContextStatus::OnTrack);
GAutoRTFMMetrics.NumTransactionsStarted++;
}
void FContext::StartNonDeferredTransaction(EMemoryValidationLevel MemoryValidationLevel)
{
AUTORTFM_ASSERT(GetNumDeferredTransactions() == 0);
AUTORTFM_ENSURE_MSG(CurrentTransaction, "FContext::StartNonDeferredTransaction() can only be called within a scoped transaction");
PushTransaction(
/* Closed */ false,
/* bIsScoped */ false,
/* StackRange */ CurrentTransaction->GetStackRange(),
/* MemoryValidationLevel */ MemoryValidationLevel);
// This form of transaction is always ultimately within a scoped Transact
AUTORTFM_ASSERT(Status == EContextStatus::OnTrack);
GAutoRTFMMetrics.NumTransactionsStarted++;
}
ETransactionResult FContext::CommitTransaction()
{
AUTORTFM_ASSERT(Status == EContextStatus::OnTrack);
ETransactionResult Result = ETransactionResult::Committed;
if (GetNumDeferredTransactions())
{
// The optimization worked! We didn't need to allocate an FTransaction for this.
NumDeferredTransactions -= 1;
}
else
{
// Scoped transactions commit on return, so committing explicitly isn't allowed
AUTORTFM_ASSERT(CurrentTransaction->IsScopedTransaction() == false);
if (CurrentTransaction->IsNested())
{
Result = ResolveNestedTransaction(CurrentTransaction);
}
else
{
AUTORTFM_VERBOSE("About to commit; my state is:");
DumpState();
AUTORTFM_VERBOSE("Committing...");
if (AttemptToCommitTransaction(CurrentTransaction))
{
Result = ETransactionResult::Committed;
}
else
{
AUTORTFM_VERBOSE("Commit failed!");
AUTORTFM_ASSERT(Status != EContextStatus::OnTrack);
AUTORTFM_ASSERT(Status != EContextStatus::Idle);
}
}
// Parent transaction is now the current transaction
PopTransaction();
}
GAutoRTFMMetrics.NumTransactionsCommitted++;
return Result;
}
void FContext::RollbackTransaction(EContextStatus NewStatus)
{
GAutoRTFMMetrics.NumTransactionsAborted++;
AUTORTFM_ASSERT(Status == EContextStatus::OnTrack);
AUTORTFM_ASSERT(IsStatusAborting(NewStatus));
Status = NewStatus;
if (GetNumDeferredTransactions())
{
// The optimization worked! We didn't need to allocate an FTransaction for this.
NumDeferredTransactions -= 1;
}
else
{
AUTORTFM_ASSERT(nullptr != CurrentTransaction);
// Sort out how aborts work
CurrentTransaction->AbortWithoutThrowing();
// Non-scoped transactions are ended immediately, but scoped need to get to the end scope before being popped
if (!CurrentTransaction->IsScopedTransaction())
{
PopTransaction();
}
}
}
void FContext::AbortTransaction(EContextStatus NewStatus)
{
RollbackTransaction(NewStatus);
Throw();
}
EContextStatus FContext::CallClosedNest(void (*ClosedFunction)(void* Arg), void* Arg)
{
FTransaction* const Transaction = GetCurrentTransaction();
AUTORTFM_ASSERT(Transaction != nullptr);
AUTORTFM_ASSERT(FTransaction::EState::OpenActive == Transaction->State());
EMemoryValidationLevel PreviousValidationLevel = Transaction->MemoryValidationLevel();
const void* PreviousOpenReturnAddress = Transaction->OpenReturnAddress();
Transaction->SetClosedActive();
PushCallNest(CallNestPool.Take(this));
CurrentNest->Try([&]() { ClosedFunction(Arg); });
PopCallNest();
if (Transaction == CurrentTransaction && Transaction->IsClosedActive()) // Transaction may have been aborted.
{
Transaction->SetOpenActive(PreviousValidationLevel, PreviousOpenReturnAddress);
}
return GetStatus();
}
void FContext::PushCallNest(FCallNest* NewCallNest)
{
AUTORTFM_ASSERT(NewCallNest != nullptr);
AUTORTFM_ASSERT(NewCallNest->Parent == nullptr);
NewCallNest->Parent = CurrentNest;
CurrentNest = NewCallNest;
}
void FContext::PopCallNest()
{
AUTORTFM_ASSERT(CurrentNest != nullptr);
FCallNest* OldCallNest = CurrentNest;
CurrentNest = CurrentNest->Parent;
CallNestPool.Return(OldCallNest);
}
FTransaction* FContext::PushTransaction(
bool bClosed,
bool bIsScoped,
FStackRange StackRange,
EMemoryValidationLevel MemoryValidationLevel)
{
AUTORTFM_ASSERT(!GetNumDeferredTransactions());
if (CurrentTransaction != nullptr)
{
AUTORTFM_ASSERT(CurrentTransaction->IsActive());
CurrentTransaction->SetInactive();
}
FTransaction* NewTransaction = TransactionPool.Take(this);
NewTransaction->Initialize(
/* Parent */ CurrentTransaction,
/* bIsScoped */ bIsScoped,
/* StackRange */ StackRange);
if (bClosed)
{
NewTransaction->SetClosedActive();
}
else
{
NewTransaction->SetOpenActive(MemoryValidationLevel, /* ReturnAddress */ nullptr);
}
CurrentTransaction = NewTransaction;
// Collect stats that we've got a new transaction.
Stats.Collect<EStatsKind::Transaction>();
return NewTransaction;
}
void FContext::PopTransaction()
{
AUTORTFM_ASSERT(!GetNumDeferredTransactions());
AUTORTFM_ASSERT(CurrentTransaction != nullptr);
AUTORTFM_ASSERT(CurrentTransaction->IsDone());
FTransaction* OldTransaction = CurrentTransaction;
CurrentTransaction = CurrentTransaction->GetParent();
if (CurrentTransaction != nullptr)
{
AUTORTFM_ASSERT(CurrentTransaction->IsInactive());
CurrentTransaction->SetActive();
}
TransactionPool.Return(OldTransaction);
}
void FContext::ClearTransactionStatus()
{
switch (Status)
{
case EContextStatus::OnTrack:
break;
case EContextStatus::AbortedByLanguage:
case EContextStatus::AbortedByRequest:
case EContextStatus::AbortedByCascadingAbort:
case EContextStatus::AbortedByCascadingRetry:
case EContextStatus::AbortedByFailedLockAcquisition:
Status = EContextStatus::OnTrack;
break;
default:
AutoRTFM::InternalUnreachable();
}
}
ETransactionResult FContext::ResolveNestedTransaction(FTransaction* Transaction)
{
if (Status == EContextStatus::OnTrack)
{
bool bCommitResult = AttemptToCommitTransaction(Transaction);
AUTORTFM_ASSERT(bCommitResult);
AUTORTFM_ASSERT(Status == EContextStatus::OnTrack);
return ETransactionResult::Committed;
}
AUTORTFM_ASSERT(Transaction->IsDone());
switch (Status)
{
case EContextStatus::AbortedByRequest:
return ETransactionResult::AbortedByRequest;
case EContextStatus::AbortedByLanguage:
return ETransactionResult::AbortedByLanguage;
case EContextStatus::AbortedByCascadingAbort:
case EContextStatus::AbortedByCascadingRetry:
return ETransactionResult::AbortedByCascade;
default:
AutoRTFM::InternalUnreachable();
}
}
AutoRTFM::FStackRange FContext::GetThreadStackRange()
{
// On some platforms, looking up the stack range is quite expensive, so caching it
// is important for performance. Linux glibc is particularly bad--see
// https://github.com/golang/go/issues/68587 for a deep dive.
thread_local FStackRange CachedStackRange = []
{
FStackRange Stack;
#if AUTORTFM_PLATFORM_WINDOWS
GetCurrentThreadStackLimits(reinterpret_cast<PULONG_PTR>(&Stack.Low), reinterpret_cast<PULONG_PTR>(&Stack.High));
#elif defined(__APPLE__)
Stack.High = pthread_get_stackaddr_np(pthread_self());
size_t StackSize = pthread_get_stacksize_np(pthread_self());
Stack.Low = static_cast<char*>(Stack.High) - StackSize;
#else
pthread_attr_t Attr{};
pthread_getattr_np(pthread_self(), &Attr);
Stack.Low = 0;
size_t StackSize = 0;
pthread_attr_getstack(&Attr, &Stack.Low, &StackSize);
Stack.High = static_cast<char*>(Stack.Low) + StackSize;
#endif
AUTORTFM_ASSERT(Stack.High > Stack.Low);
return Stack;
}();
return CachedStackRange;
}
ETransactionResult FContext::Transact(void (*UninstrumentedFunction)(void*), void (*InstrumentedFunction)(void*), void* Arg)
{
if (AUTORTFM_UNLIKELY(EContextStatus::Committing == Status))
{
return ETransactionResult::AbortedByTransactDuringCommit;
}
if (AUTORTFM_UNLIKELY(IsAborting()))
{
return ETransactionResult::AbortedByTransactDuringAbort;
}
AUTORTFM_ASSERT(Status == EContextStatus::Idle || Status == EContextStatus::OnTrack);
if (!InstrumentedFunction)
{
AUTORTFM_WARN("Could not find function in AutoRTFM::FContext::Transact");
return ETransactionResult::AbortedByLanguage;
}
// TODO: We could do better if we ever need to. There is no fundamental
// reason we can't have a "range" of deferred transactions in the middle
// of the transaction stack.
MaterializeDeferredTransactions();
AUTORTFM_ASSERT(!GetNumDeferredTransactions());
FCallNest* NewNest = CallNestPool.Take(this);
void* TransactStackStart = &NewNest;
ETransactionResult Result = ETransactionResult::Committed; // Initialize to something to make the compiler happy.
if (!CurrentTransaction)
{
// If exceptions are enabled, then ensure that the transaction is automatically committed if
// an exception is thrown inside the transaction and the handler is outside the transaction.
struct FAutoCommitter final
{
FAutoCommitter(FContext& Context) : Context{Context} {}
~FAutoCommitter()
{
if (bCommitOnDestruct)
{
Commit();
}
}
void Commit()
{
bCommitOnDestruct = false;
if (!Context.CurrentTransaction->IsDone())
{
Context.CurrentTransaction->SetDone();
}
Context.PopCallNest();
Context.PopTransaction();
Context.ClearTransactionStatus();
AUTORTFM_ASSERT(Context.CurrentNest == nullptr);
AUTORTFM_ASSERT(Context.CurrentTransaction == nullptr);
Context.Reset();
}
private:
FContext& Context;
bool bCommitOnDestruct = true;
};
FAutoCommitter AutoCommitter(*this);
AUTORTFM_ASSERT(Status == EContextStatus::Idle);
AUTORTFM_ASSERT(CurrentThreadId == FThreadID::Invalid);
CurrentThreadId = FThreadID::GetCurrent();
AUTORTFM_ASSERT(Stack == FStackRange{});
Stack = GetThreadStackRange();
AUTORTFM_ASSERT(Stack.Contains(TransactStackStart));
FTransaction* NewTransaction = PushTransaction(
/* Closed */ true,
/* bIsScoped */ true,
/* StackRange */ {Stack.Low, &TransactStackStart},
/* MemoryValidationLevel */ EMemoryValidationLevel::Disabled);
PushCallNest(NewNest);
bool bTriedToRunOnce = false;
for (;;)
{
Status = EContextStatus::OnTrack;
AUTORTFM_ASSERT(CurrentTransaction->IsFresh());
CurrentNest->Try([&] () { InstrumentedFunction(Arg); });
AUTORTFM_ASSERT(CurrentTransaction == NewTransaction); // The transaction lambda should have unwound any nested transactions.
AUTORTFM_ASSERT(Status != EContextStatus::Idle);
switch (Status)
{
case EContextStatus::OnTrack:
AUTORTFM_VERBOSE("About to commit; my state is:");
DumpState();
AUTORTFM_VERBOSE("Committing...");
if (AUTORTFM_UNLIKELY(!bTriedToRunOnce && AutoRTFM::ForTheRuntime::ShouldRetryNonNestedTransactions()))
{
// We skip trying to commit this time, and instead re-run the transaction.
Status = EContextStatus::AbortedByFailedLockAcquisition;
CurrentTransaction->AbortWithoutThrowing();
ClearTransactionStatus();
// We've tried to run at least once if we get here!
CurrentTransaction->Reset();
CurrentTransaction->SetClosedActive();
bTriedToRunOnce = true;
continue;
}
if (AttemptToCommitTransaction(CurrentTransaction))
{
Result = ETransactionResult::Committed;
break;
}
AUTORTFM_VERBOSE("Commit failed!");
AUTORTFM_ASSERT(Status != EContextStatus::OnTrack);
AUTORTFM_ASSERT(Status != EContextStatus::Idle);
break;
case EContextStatus::AbortedByRequest:
Result = ETransactionResult::AbortedByRequest;
break;
case EContextStatus::AbortedByLanguage:
Result = ETransactionResult::AbortedByLanguage;
break;
case EContextStatus::AbortedByCascadingAbort:
Result = ETransactionResult::AbortedByCascade;
break;
case EContextStatus::AbortedByCascadingRetry:
// Clean up the transaction to get it ready for re-execution.
ClearTransactionStatus();
CurrentTransaction->Reset();
AUTORTFM_ASSERT(Status == EContextStatus::OnTrack);
// Then get rolling!
CurrentTransaction->SetClosedActive();
// Lastly check whether the AutoRTFM runtime was disabled during
// the call to `PostAbortCallback`, and if so just execute the
// function without AutoRTFM as a fallback.
if (!ForTheRuntime::IsAutoRTFMRuntimeEnabled())
{
UninstrumentedFunction(Arg);
Result = ETransactionResult::Committed;
break;
}
continue;
case EContextStatus::AbortedByFailedLockAcquisition:
continue; // Retry the transaction
default:
Unreachable();
}
break;
}
AutoCommitter.Commit();
}
else
{
// This transaction is within another transaction
// If exceptions are enabled, then ensure that the transaction is automatically committed if
// an exception is thrown inside the transaction and the handler is outside the transaction.
struct FAutoCommitter final
{
FAutoCommitter(FContext& Context) : Context{Context} {}
~FAutoCommitter()
{
if (bCommitOnDestruct)
{
Apply();
}
}
ETransactionResult Apply()
{
bCommitOnDestruct = false;
ETransactionResult Result = Context.ResolveNestedTransaction(Context.CurrentTransaction);
Context.PopCallNest();
Context.PopTransaction();
AUTORTFM_ASSERT(Context.CurrentNest != nullptr);
AUTORTFM_ASSERT(Context.CurrentTransaction != nullptr);
if (Result == ETransactionResult::AbortedByCascade)
{
// Cascading aborts continue to abort transactions until reaching
// a non-scoped transaction, or abort all transactions if the
// transaction stack contains only scoped transactions.
if (Context.CurrentTransaction->IsScopedTransaction())
{
Context.CurrentTransaction->AbortAndThrow(); // note: does not return
}
}
else
{
Context.ClearTransactionStatus();
}
return Result;
}
private:
FContext& Context;
bool bCommitOnDestruct = true;
};
FAutoCommitter AutoCommitter(*this);
AUTORTFM_ASSERT(Status == EContextStatus::OnTrack);
AUTORTFM_ASSERT(CurrentThreadId == FThreadID::GetCurrent());
AUTORTFM_ASSERT(Stack.Contains(TransactStackStart));
FTransaction* NewTransaction = PushTransaction(
/* Closed */ true,
/* bIsScoped */ true,
/* StackRange */ {Stack.Low, &TransactStackStart},
/* MemoryValidationLevel */ EMemoryValidationLevel::Disabled);
PushCallNest(NewNest);
bool bTriedToRunOnce = false;
for (;;)
{
CurrentNest->Try([&]() { InstrumentedFunction(Arg); });
AUTORTFM_ASSERT(CurrentTransaction == NewTransaction);
if (Status == EContextStatus::OnTrack)
{
if (AUTORTFM_UNLIKELY(!bTriedToRunOnce && AutoRTFM::ForTheRuntime::ShouldRetryNestedTransactionsToo()))
{
// We skip trying to commit this time, and instead re-run the transaction.
Status = EContextStatus::AbortedByFailedLockAcquisition;
NewTransaction->AbortWithoutThrowing();
ClearTransactionStatus();
// We've tried to run at least once if we get here!
CurrentTransaction->Reset();
CurrentTransaction->SetClosedActive();
bTriedToRunOnce = true;
continue;
}
}
break;
}
Result = AutoCommitter.Apply();
}
return Result;
}
void FContext::AbortByRequestAndThrow()
{
AUTORTFM_ASSERT(Status == EContextStatus::OnTrack);
GAutoRTFMMetrics.NumTransactionsAbortedByRequest++;
RollbackTransaction(EContextStatus::AbortedByRequest);
Throw();
}
void FContext::AbortByLanguageAndThrow()
{
AUTORTFM_ASSERT(Status == EContextStatus::OnTrack);
GAutoRTFMMetrics.NumTransactionsAbortedByLanguage++;
RollbackTransaction(EContextStatus::AbortedByLanguage);
Throw();
}
void FContext::Reset()
{
AUTORTFM_ASSERT(CurrentThreadId == FThreadID::GetCurrent() || CurrentThreadId == FThreadID::Invalid);
CurrentThreadId = FThreadID::Invalid;
Stack = {};
CurrentTransaction = nullptr;
CurrentNest = nullptr;
Status = EContextStatus::Idle;
StackLocalInitializerDepth = 0;
TaskPool.Reset();
}
void FContext::Throw()
{
GetCurrentNest()->AbortJump.Throw();
}
void FContext::DumpState() const
{
AUTORTFM_VERBOSE("Context at %p", this);
}
} // namespace AutoRTFM
#endif // defined(__AUTORTFM) && __AUTORTFM