Files
UnrealEngine/Engine/Source/Runtime/AutoRTFM/Private/Transaction.cpp
2025-05-18 13:04:45 +08:00

511 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#if (defined(__AUTORTFM) && __AUTORTFM)
#include "Transaction.h"
#include "TransactionInlines.h"
#include "CallNestInlines.h"
#include "ContextStatus.h"
#include "HitSet.h"
#include "Utils.h"
#include <cstddef>
#include <cstdint>
namespace AutoRTFM
{
void FTransaction::Resurrect(FContext* const InContext)
{
AUTORTFM_ASSERT(Context == InContext)
Parent = nullptr;
StatDepth = 1;
RecordedWriteHash = 0;
NumWriteLogsHashed = 0;
CurrentMemoryValidationLevel = EMemoryValidationLevel::Disabled;
CurrentOpenReturnAddress = nullptr;
CurrentState = EState::Uninitialized;
bIsStackScoped = false;
bIsInAllocateFn = false;
}
void FTransaction::Suppress()
{
CurrentState = EState::Done;
Reset();
}
FTransaction** FTransaction::GetIntrusiveAddress()
{
return &Parent;
}
FTransaction::FTransaction(FContext* Context)
: Context(Context)
, CommitTasks(Context->GetTaskPool())
, AbortTasks(Context->GetTaskPool())
{
Resurrect(Context);
}
FTransaction::~FTransaction()
{
Suppress();
}
void FTransaction::Initialize(FTransaction* Parent_, bool bIsStackScoped_, FStackRange StackRange_)
{
AUTORTFM_ASSERT(CurrentState == EState::Uninitialized);
Parent = Parent_;
bIsStackScoped = bIsStackScoped_;
StackRange = StackRange_;
// For stats, record the nested depth of the transaction.
if (Parent)
{
StatDepth = Parent->StatDepth + 1;
}
Stats.Collect<EStatsKind::AverageTransactionDepth>(StatDepth);
Stats.Collect<EStatsKind::MaximumTransactionDepth>(StatDepth);
}
void FTransaction::Reset()
{
AUTORTFM_ASSERT(IsDone());
CommitTasks.Reset();
AbortTasks.Reset();
HitSet.Reset();
NewMemoryTracker.Reset();
WriteLog.Reset();
CurrentMemoryValidationLevel = EMemoryValidationLevel::Disabled;
// Reset to the initial state.
CurrentState = EState::Uninitialized;
DeferredPopOnCommitHandlers.Reset();
DeferredPopOnAbortHandlers.Reset();
DeferredPopAllOnCommitHandlers.Reset();
DeferredPopAllOnAbortHandlers.Reset();
AUTORTFM_ASSERT(IsFresh());
}
bool FTransaction::IsFresh() const
{
return HitSet.IsEmpty()
&& NewMemoryTracker.IsEmpty()
&& WriteLog.IsEmpty()
&& CommitTasks.IsEmpty()
&& AbortTasks.IsEmpty()
&& !IsDone()
&& DeferredPopOnCommitHandlers.IsEmpty()
&& DeferredPopOnAbortHandlers.IsEmpty()
&& DeferredPopAllOnCommitHandlers.IsEmpty()
&& DeferredPopAllOnAbortHandlers.IsEmpty();
}
void FTransaction::AbortWithoutThrowing()
{
AUTORTFM_VERBOSE("Aborting '%hs'!", GetContextStatusName(Context->GetStatus()));
AUTORTFM_ASSERT(Context->IsAborting());
Stats.Collect<EStatsKind::Abort>();
CollectStats();
// Ensure that we enter the done state before applying the commit, as this
// will ensure the open-memory validation is performed before the write log
// is cleared.
SetDone();
// Call the destructors of all the OnCommit functors before undoing the transactional memory and
// calling the OnAbort callbacks. This is important as the callback functions may have captured
// variables that are depending on the allocated memory.
CommitTasks.Reset();
Undo();
AbortTasks.RemoveEachBackward([&](TTask<void()>& Task)
{
Task();
});
if (IsNested())
{
AUTORTFM_ASSERT(Parent);
}
else
{
AUTORTFM_ASSERT(Context->IsAborting());
}
}
void FTransaction::AbortAndThrow()
{
AbortWithoutThrowing();
Context->Throw();
}
bool FTransaction::AttemptToCommit()
{
AUTORTFM_ASSERT(Context->GetStatus() == EContextStatus::Committing);
AUTORTFM_ASSERT(Context->GetCurrentTransaction() == this);
Stats.Collect<EStatsKind::Commit>();
CollectStats();
// Ensure that we enter the done state before applying the commit, as this
// will ensure the open-memory validation is performed before the write log
// is cleared.
SetDone();
bool bResult;
if (IsNested())
{
CommitNested();
bResult = true;
}
else
{
bResult = AttemptToCommitOuterNest();
}
return bResult;
}
void FTransaction::Undo()
{
AUTORTFM_VERBOSE("Undoing a transaction...");
AUTORTFM_ASSERT(IsDone());
for(auto Iter = WriteLog.rbegin(); Iter != WriteLog.rend(); ++Iter)
{
FWriteLogEntry Entry = *Iter;
// No write records should be within the transaction's stack range.
AUTORTFM_ENSURE(!IsOnStack(Entry.LogicalAddress));
memcpy(Entry.LogicalAddress, Entry.Data, Entry.Size);
}
AUTORTFM_VERBOSE("Undone a transaction!");
}
void FTransaction::CommitNested()
{
AUTORTFM_ASSERT(Parent);
// We need to pass our write log to our parent transaction, but with care!
// We need to discard any writes if the memory location is on the parent
// transaction's stack range.
for (FWriteLogEntry Write : WriteLog)
{
if (Parent->IsOnStack(Write.LogicalAddress))
{
continue;
}
if (Write.Size <= FHitSet::MaxSize)
{
FHitSetEntry HitSetEntry{};
HitSetEntry.Address = reinterpret_cast<uintptr_t>(Write.LogicalAddress);
HitSetEntry.Size = static_cast<uint16_t>(Write.Size);
HitSetEntry.bNoMemoryValidation = Write.bNoMemoryValidation;
if (Parent->HitSet.FindOrTryInsert(HitSetEntry))
{
continue; // Don't duplicate the write-log entry.
}
}
Parent->WriteLog.Push(Write);
}
// For all the deferred calls to `PopOnCommitHandler` that we couldn't
// process (because our transaction nest didn't `PushOnCommitHandler`)
// we need to move these to the parent now to handle them.
for (const void* Key : DeferredPopOnCommitHandlers)
{
Parent->PopDeferUntilCommitHandler(Key);
}
DeferredPopOnCommitHandlers.Reset();
// For all the deferred calls to `PopOnAbortHandler` that we couldn't
// process (because our transaction nest didn't `PushOnAbortHandler`)
// we need to move these to the parent now to handle them.
for (const void* Key : DeferredPopOnAbortHandlers)
{
Parent->PopDeferUntilAbortHandler(Key);
}
DeferredPopOnAbortHandlers.Reset();
// For all the calls to `PopAllOnCommitHandlers` we need to run these
// again on parent now to handle them there too.
for (const void* Key : DeferredPopAllOnCommitHandlers)
{
Parent->PopAllDeferUntilCommitHandlers(Key);
}
DeferredPopAllOnCommitHandlers.Reset();
// For all the calls to `PopAllOnAbortHandlers` we need to run these
// again on parent now to handle them there too.
for (const void* Key : DeferredPopAllOnAbortHandlers)
{
Parent->PopAllDeferUntilAbortHandlers(Key);
}
DeferredPopAllOnAbortHandlers.Reset();
Parent->CommitTasks.AddAll(std::move(CommitTasks));
Parent->AbortTasks.AddAll(std::move(AbortTasks));
Parent->NewMemoryTracker.Merge(NewMemoryTracker);
}
bool FTransaction::AttemptToCommitOuterNest()
{
AUTORTFM_ASSERT(!Parent);
AUTORTFM_VERBOSE("About to run commit tasks!");
Context->DumpState();
AUTORTFM_VERBOSE("Running commit tasks...");
AbortTasks.Reset();
CommitTasks.RemoveEachForward([] (TTask<void()>& Task)
{
Task();
});
return true;
}
void FTransaction::SetOpenActiveValidatorEnabled(EMemoryValidationLevel NewMemoryValidationLevel, const void* ReturnAddress)
{
AUTORTFM_ASSERT(NewMemoryValidationLevel != EMemoryValidationLevel::Disabled);
CurrentMemoryValidationLevel = NewMemoryValidationLevel;
CurrentOpenReturnAddress = ReturnAddress;
if (AutoRTFM::ForTheRuntime::GetMemoryValidationThrottlingEnabled())
{
FOpenHashThrottler& Throttler = Context->GetOpenHashThrottler();
if (!Throttler.ShouldHashFor(ReturnAddress))
{
CurrentMemoryValidationLevel = EMemoryValidationLevel::Disabled;
}
Throttler.Update();
}
SetState<EState::OpenActive>();
}
void FTransaction::SetOpenActive(EMemoryValidationLevel NewMemoryValidationLevel, const void* ReturnAddress)
{
if (AUTORTFM_UNLIKELY(NewMemoryValidationLevel != EMemoryValidationLevel::Disabled))
{
AUTORTFM_MUST_TAIL return SetOpenActiveValidatorEnabled(NewMemoryValidationLevel, ReturnAddress);
}
CurrentMemoryValidationLevel = NewMemoryValidationLevel;
CurrentOpenReturnAddress = ReturnAddress;
// TODO: Validate if open -> open with different validation levels.
RecordedWriteHash = 0;
NumWriteLogsHashed = 0;
CurrentOpenReturnAddress = nullptr;
SetState<EState::OpenActive>();
}
void FTransaction::SetClosedActive()
{
SetState<EState::ClosedActive>();
}
void FTransaction::SetOpenInactive()
{
SetState<EState::OpenInactive>();
}
void FTransaction::SetClosedInactive()
{
SetState<EState::ClosedInactive>();
}
void FTransaction::SetActive()
{
switch (CurrentState)
{
case EState::OpenActive:
case EState::ClosedActive:
break;
case EState::OpenInactive:
SetState<EState::OpenActive>();
break;
case EState::ClosedInactive:
SetState<EState::ClosedActive>();
break;
default:
AUTORTFM_FATAL("Invalid state");
}
}
void FTransaction::SetInactive()
{
switch (CurrentState)
{
case EState::OpenInactive:
case EState::ClosedInactive:
break;
case EState::OpenActive:
SetState<EState::OpenInactive>();
break;
case EState::ClosedActive:
SetState<EState::ClosedInactive>();
break;
default:
AUTORTFM_FATAL("Invalid state");
}
}
void FTransaction::SetDone()
{
SetState<EState::Done>();
}
template<FTransaction::EState NewState>
void FTransaction::SetState()
{
AUTORTFM_ASSERT(NewState != CurrentState);
switch (CurrentState)
{
case EState::Uninitialized:
AUTORTFM_ASSERT(NewState == EState::OpenActive || NewState == EState::ClosedActive);
break;
// OpenActive -> OpenInactive, ClosedActive or Done
case EState::OpenActive:
AUTORTFM_ASSERT(NewState == EState::OpenInactive || NewState == EState::ClosedActive || NewState == EState::Done);
if (CurrentMemoryValidationLevel != EMemoryValidationLevel::Disabled)
{
ValidateWriteHash();
RecordedWriteHash = 0;
NumWriteLogsHashed = 0;
}
else
{
AUTORTFM_ASSERT(RecordedWriteHash == 0 && NumWriteLogsHashed == 0);
}
break;
// ClosedActive -> ClosedInactive, OpenActive or Done
case EState::ClosedActive:
AUTORTFM_ASSERT(NewState == EState::ClosedInactive || NewState == EState::OpenActive || NewState == EState::Done);
break;
// ClosedActive -> OpenActive
case EState::OpenInactive:
AUTORTFM_ASSERT(NewState == EState::OpenActive);
break;
// ClosedInactive -> ClosedActive
case EState::ClosedInactive:
AUTORTFM_ASSERT(NewState == EState::ClosedActive);
break;
case EState::Done:
AUTORTFM_FATAL("Once Done, the transaction cannot change state without a call to Reset()");
break;
default:
AUTORTFM_FATAL("Invalid state");
break;
}
// OpenInactive, ClosedActive or Done -> OpenActive
if (NewState == EState::OpenActive) {
if (CurrentMemoryValidationLevel != EMemoryValidationLevel::Disabled)
{
AUTORTFM_ASSERT(RecordedWriteHash == 0 && NumWriteLogsHashed == 0);
RecordWriteHash();
}
}
CurrentState = NewState;
}
void FTransaction::DebugBreakIfMemoryValidationFails()
{
if (CurrentMemoryValidationLevel != EMemoryValidationLevel::Disabled)
{
FWriteHash OldHash = RecordedWriteHash;
FWriteHash NewHash = CalculateNestedWriteHash();
if (OldHash != NewHash)
{
AUTORTFM_WARN("DebugBreakIfInvalidMemoryHash() detected a change in hash");
__builtin_debugtrap();
}
}
}
void FTransaction::RecordWriteHash()
{
NumWriteLogsHashed = WriteLog.Num();
RecordedWriteHash = CalculateNestedWriteHash();
Context->GetOpenHashThrottler().Update();
}
void FTransaction::ValidateWriteHash() const
{
FWriteHash OldHash = RecordedWriteHash;
FWriteHash NewHash = CalculateNestedWriteHash();
Context->GetOpenHashThrottler().Update();
static constexpr const char Message[] =
"Memory modified in a transaction was also modified in an call to AutoRTFM::Open(). "
"This may lead to memory corruption if the transaction is aborted.";
if (AUTORTFM_UNLIKELY(OldHash != NewHash))
{
if (CurrentMemoryValidationLevel == EMemoryValidationLevel::Warn)
{
AUTORTFM_WARN(Message);
}
else
{
if (!ForTheRuntime::GetEnsureOnInternalAbort())
{
AUTORTFM_FATAL(Message);
}
else
{
AUTORTFM_ENSURE_MSG(OldHash == NewHash, Message);
}
}
}
}
FTransaction::FWriteHash FTransaction::CalculateNestedWriteHash() const
{
return CalculateNestedWriteHashWithLimit(NumWriteLogsHashed, CurrentOpenReturnAddress);
}
FTransaction::FWriteHash FTransaction::CalculateNestedWriteHashWithLimit(size_t NumWriteEntries, const void *OpenReturnAddress) const
{
FWriteHash Hash = 0;
if (nullptr != Parent)
{
Hash = 31 * Parent->CalculateNestedWriteHashWithLimit(Parent->WriteLog.Num(), OpenReturnAddress);
}
{
FOpenHashThrottler::FHashScope Profile(Context->GetOpenHashThrottler(), OpenReturnAddress, WriteLog);
Hash ^= WriteLog.Hash(NumWriteEntries);
}
return Hash;
}
} // namespace AutoRTFM
#endif // defined(__AUTORTFM) && __AUTORTFM