1019 lines
32 KiB
C++
1019 lines
32 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#if WITH_STATETREE_TRACE_DEBUGGER
|
|
|
|
#include "Debugger/StateTreeDebugger.h"
|
|
#include "Debugger/IStateTreeTraceProvider.h"
|
|
#include "Debugger/StateTreeTraceProvider.h"
|
|
#include "Debugger/StateTreeTraceTypes.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "StateTreeDelegates.h"
|
|
#include "StateTreeModule.h"
|
|
#include "Trace/StoreClient.h"
|
|
#include "TraceServices/AnalysisService.h"
|
|
#include "TraceServices/ITraceServicesModule.h"
|
|
#include "TraceServices/Model/AnalysisSession.h"
|
|
#include "TraceServices/Model/Frames.h"
|
|
#include "Trace/Analyzer.h"
|
|
#include "Trace/Analysis.h"
|
|
#include "TraceServices/Model/Diagnostics.h"
|
|
#include "GenericPlatform/GenericPlatformMisc.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "StateTreeDebugger"
|
|
|
|
//----------------------------------------------------------------//
|
|
// UE::StateTreeDebugger
|
|
//----------------------------------------------------------------//
|
|
namespace UE::StateTreeDebugger
|
|
{
|
|
struct FDiagnosticsSessionAnalyzer : public UE::Trace::IAnalyzer
|
|
{
|
|
virtual void OnAnalysisBegin(const FOnAnalysisContext& Context) override
|
|
{
|
|
auto& Builder = Context.InterfaceBuilder;
|
|
Builder.RouteEvent(RouteId_Session2, "Diagnostics", "Session2");
|
|
}
|
|
|
|
virtual bool OnEvent(const uint16 RouteId, EStyle, const FOnEventContext& Context) override
|
|
{
|
|
const FEventData& EventData = Context.EventData;
|
|
|
|
switch (RouteId)
|
|
{
|
|
case RouteId_Session2:
|
|
{
|
|
EventData.GetString("Platform", SessionInfo.Platform);
|
|
EventData.GetString("AppName", SessionInfo.AppName);
|
|
EventData.GetString("CommandLine", SessionInfo.CommandLine);
|
|
EventData.GetString("Branch", SessionInfo.Branch);
|
|
EventData.GetString("BuildVersion", SessionInfo.BuildVersion);
|
|
SessionInfo.Changelist = EventData.GetValue<uint32>("Changelist", 0);
|
|
SessionInfo.ConfigurationType = (EBuildConfiguration) EventData.GetValue<uint8>("ConfigurationType");
|
|
SessionInfo.TargetType = (EBuildTargetType) EventData.GetValue<uint8>("TargetType");
|
|
|
|
return false;
|
|
}
|
|
default: ;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
enum : uint16
|
|
{
|
|
RouteId_Session2,
|
|
};
|
|
|
|
TraceServices::FSessionInfo SessionInfo;
|
|
};
|
|
|
|
} // UE::StateTreeDebugger
|
|
|
|
|
|
//----------------------------------------------------------------//
|
|
// FStateTreeDebugger
|
|
//----------------------------------------------------------------//
|
|
FStateTreeDebugger::FStateTreeDebugger()
|
|
: StateTreeModule(FModuleManager::GetModuleChecked<IStateTreeModule>("StateTreeModule"))
|
|
, ScrubState(EventCollections)
|
|
{
|
|
TracingStateChangedHandle = UE::StateTree::Delegates::OnTracingStateChanged.AddLambda([this](const EStateTreeTraceStatus TraceStatus)
|
|
{
|
|
// StateTree traces got enabled in the current process so let's analyse it if not already analysing something.
|
|
if (TraceStatus == EStateTreeTraceStatus::TracesStarted && !IsAnalysisSessionActive())
|
|
{
|
|
RequestAnalysisOfLatestTrace();
|
|
}
|
|
});
|
|
|
|
TracingTimelineScrubbedHandle = UE::StateTree::Delegates::OnTracingTimelineScrubbed.AddLambda([this](const double InScrubTime)
|
|
{
|
|
SetScrubTime(InScrubTime);
|
|
});
|
|
}
|
|
|
|
FStateTreeDebugger::~FStateTreeDebugger()
|
|
{
|
|
UE::StateTree::Delegates::OnTracingStateChanged.Remove(TracingStateChangedHandle);
|
|
TracingStateChangedHandle.Reset();
|
|
|
|
UE::StateTree::Delegates::OnTracingTimelineScrubbed.Remove(TracingTimelineScrubbedHandle);
|
|
TracingTimelineScrubbedHandle.Reset();
|
|
|
|
StopSessionAnalysis();
|
|
}
|
|
|
|
void FStateTreeDebugger::Tick(const float DeltaTime)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FStateTreeDebugger::Tick);
|
|
|
|
if (RetryLoadNextLiveSessionTimer > 0.0f)
|
|
{
|
|
// We are still not connected to the last live session.
|
|
// Update polling timer and retry with remaining time; 0 or less will stop retries.
|
|
if (TryStartNewLiveSessionAnalysis(RetryLoadNextLiveSessionTimer - DeltaTime))
|
|
{
|
|
RetryLoadNextLiveSessionTimer = 0.0f;
|
|
LastLiveSessionId = INDEX_NONE;
|
|
}
|
|
}
|
|
|
|
if (bSessionAnalysisPaused == false && StateTreeAsset.IsValid())
|
|
{
|
|
SyncToCurrentSessionDuration();
|
|
}
|
|
}
|
|
|
|
void FStateTreeDebugger::StopSessionAnalysis()
|
|
{
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
Session->Stop(true);
|
|
AnalysisSession.Reset();
|
|
}
|
|
|
|
bSessionAnalysisPaused = false;
|
|
HitBreakpoint.Reset();
|
|
}
|
|
|
|
void FStateTreeDebugger::SyncToCurrentSessionDuration()
|
|
{
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
AnalysisDuration = Session->GetDurationSeconds();
|
|
}
|
|
ReadTrace(AnalysisDuration);
|
|
}
|
|
}
|
|
|
|
const UE::StateTreeDebugger::FInstanceDescriptor* FStateTreeDebugger::GetInstanceDescriptor(const FStateTreeInstanceDebugId InstanceId) const
|
|
{
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
|
|
if (const IStateTreeTraceProvider* Provider = Session->ReadProvider<IStateTreeTraceProvider>(FStateTreeTraceProvider::ProviderName))
|
|
{
|
|
return Provider->GetInstanceDescriptor(InstanceId).Get();
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
FText FStateTreeDebugger::GetInstanceName(const FStateTreeInstanceDebugId InstanceId) const
|
|
{
|
|
const UE::StateTreeDebugger::FInstanceDescriptor* FoundDescriptor = GetInstanceDescriptor(InstanceId);
|
|
return (FoundDescriptor != nullptr) ? FText::FromString(FoundDescriptor->Name) : LOCTEXT("InstanceNotFound","Instance not found");
|
|
}
|
|
|
|
FText FStateTreeDebugger::GetInstanceDescription(const FStateTreeInstanceDebugId InstanceId) const
|
|
{
|
|
const UE::StateTreeDebugger::FInstanceDescriptor* FoundDescriptor = GetInstanceDescriptor(InstanceId);
|
|
return (FoundDescriptor != nullptr) ? DescribeInstance(*FoundDescriptor) : LOCTEXT("InstanceNotFound","Instance not found");
|
|
}
|
|
|
|
void FStateTreeDebugger::SelectInstance(const FStateTreeInstanceDebugId InstanceId)
|
|
{
|
|
if (SelectedInstanceId != InstanceId)
|
|
{
|
|
SelectedInstanceId = InstanceId;
|
|
|
|
// Notify so listener can cleanup anything related to previous instance
|
|
OnSelectedInstanceCleared.ExecuteIfBound();
|
|
|
|
// Update event collection index for newly debugged instance
|
|
SetScrubStateCollectionIndex(InstanceId.IsValid()
|
|
? EventCollections.IndexOfByPredicate([InstanceId = InstanceId](const UE::StateTreeDebugger::FInstanceEventCollection& Entry)
|
|
{
|
|
return Entry.InstanceId == InstanceId;
|
|
})
|
|
: INDEX_NONE);
|
|
}
|
|
}
|
|
|
|
// Deprecated
|
|
void FStateTreeDebugger::GetSessionInstances(TArray<UE::StateTreeDebugger::FInstanceDescriptor>& OutInstances) const
|
|
{
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
|
|
if (const IStateTreeTraceProvider* Provider = Session->ReadProvider<IStateTreeTraceProvider>(FStateTreeTraceProvider::ProviderName))
|
|
{
|
|
Provider->GetInstances(OutInstances);
|
|
}
|
|
}
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
}
|
|
|
|
void FStateTreeDebugger::GetSessionInstanceDescriptors(TArray<const TSharedRef<const UE::StateTreeDebugger::FInstanceDescriptor>>& OutInstances) const
|
|
{
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
|
|
if (const IStateTreeTraceProvider* Provider = Session->ReadProvider<IStateTreeTraceProvider>(FStateTreeTraceProvider::ProviderName))
|
|
{
|
|
Provider->GetInstances(OutInstances);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FStateTreeDebugger::RequestAnalysisOfEditorSession()
|
|
{
|
|
// Get snapshot of current trace to help identify the next live one
|
|
TArray<FTraceDescriptor> TraceDescriptors;
|
|
GetLiveTraces(TraceDescriptors);
|
|
LastLiveSessionId = TraceDescriptors.Num() ? TraceDescriptors.Last().TraceId : INDEX_NONE;
|
|
|
|
// 0 is the invalid value used for Trace Id
|
|
constexpr int32 InvalidTraceId = 0;
|
|
int32 ActiveTraceId = InvalidTraceId;
|
|
|
|
// StartTraces returns true if a new connection was created. In this case we will receive OnTracingStateChanged
|
|
// and we'll try to start an analysis on that new connection as soon as possible.
|
|
// Otherwise it might have been able to use an active connection in which case it was returned in the output parameter.
|
|
if (StateTreeModule.StartTraces(ActiveTraceId))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Otherwise we start analysis of the already active trace, if any.
|
|
if (ActiveTraceId != InvalidTraceId)
|
|
{
|
|
if (const FTraceDescriptor* Descriptor = TraceDescriptors.FindByPredicate([ActiveTraceId](const FTraceDescriptor& Descriptor)
|
|
{
|
|
return Descriptor.TraceId == ActiveTraceId;
|
|
}))
|
|
{
|
|
return RequestSessionAnalysis(*Descriptor);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FStateTreeDebugger::RequestAnalysisOfLatestTrace()
|
|
{
|
|
// Invalidate our current active session
|
|
ActiveSessionTraceDescriptor = FTraceDescriptor();
|
|
|
|
// Stop current analysis if any
|
|
StopSessionAnalysis();
|
|
|
|
// This might not succeed immediately but will schedule next retry if necessary
|
|
TryStartNewLiveSessionAnalysis(1.0f);
|
|
}
|
|
|
|
|
|
bool FStateTreeDebugger::TryStartNewLiveSessionAnalysis(const float RetryPollingDuration)
|
|
{
|
|
TArray<FTraceDescriptor> Traces;
|
|
GetLiveTraces(Traces);
|
|
|
|
if (Traces.Num() && Traces.Last().TraceId != LastLiveSessionId)
|
|
{
|
|
// Intentional call to StartSessionAnalysis instead of RequestSessionAnalysis since we want
|
|
// to set 'bIsAnalyzingNextEditorSession' before calling OnNewSession delegate.
|
|
const bool bStarted = StartSessionAnalysis(Traces.Last());
|
|
if (bStarted)
|
|
{
|
|
UpdateAnalysisTransitionType(EAnalysisSourceType::EditorSession);
|
|
|
|
SetScrubStateCollectionIndex(INDEX_NONE);
|
|
OnNewSession.ExecuteIfBound();
|
|
}
|
|
|
|
return bStarted;
|
|
}
|
|
|
|
RetryLoadNextLiveSessionTimer = RetryPollingDuration;
|
|
UE_CLOG(RetryLoadNextLiveSessionTimer > 0, LogStateTree, Log, TEXT("Unable to start analysis for the most recent live session."));
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FStateTreeDebugger::StartSessionAnalysis(const FTraceDescriptor& TraceDescriptor)
|
|
{
|
|
if (ActiveSessionTraceDescriptor == TraceDescriptor)
|
|
{
|
|
return ActiveSessionTraceDescriptor.IsValid();
|
|
}
|
|
|
|
ActiveSessionTraceDescriptor = FTraceDescriptor();
|
|
|
|
// Make sure any active analysis is stopped
|
|
StopSessionAnalysis();
|
|
|
|
UE::Trace::FStoreClient* StoreClient = GetStoreClient();
|
|
if (StoreClient == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If new trace descriptor is not valid no need to continue
|
|
if (TraceDescriptor.IsValid() == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AnalysisDuration = 0;
|
|
LastTraceReadTime = 0;
|
|
|
|
const uint32 TraceId = TraceDescriptor.TraceId;
|
|
|
|
// Make sure it is still live
|
|
const UE::Trace::FStoreClient::FSessionInfo* SessionInfo = StoreClient->GetSessionInfoByTraceId(TraceId);
|
|
if (SessionInfo != nullptr)
|
|
{
|
|
UE::Trace::FStoreClient::FTraceData TraceData = StoreClient->ReadTrace(TraceId);
|
|
if (!TraceData)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FString TraceName(StoreClient->GetStatus()->GetStoreDir());
|
|
const UE::Trace::FStoreClient::FTraceInfo* TraceInfo = StoreClient->GetTraceInfoById(TraceId);
|
|
if (TraceInfo != nullptr)
|
|
{
|
|
FString Name(TraceInfo->GetName());
|
|
if (!Name.EndsWith(TEXT(".utrace")))
|
|
{
|
|
Name += TEXT(".utrace");
|
|
}
|
|
TraceName = FPaths::Combine(TraceName, Name);
|
|
FPaths::NormalizeFilename(TraceName);
|
|
}
|
|
|
|
ITraceServicesModule& TraceServicesModule = FModuleManager::LoadModuleChecked<ITraceServicesModule>("TraceServices");
|
|
if (const TSharedPtr<TraceServices::IAnalysisService> TraceAnalysisService = TraceServicesModule.GetAnalysisService())
|
|
{
|
|
checkf(!AnalysisSession.IsValid(), TEXT("Must make sure that current session was properly stopped before starting a new one otherwise it can cause threading issues"));
|
|
AnalysisSession = TraceAnalysisService->StartAnalysis(TraceId, *TraceName, MoveTemp(TraceData));
|
|
}
|
|
|
|
if (AnalysisSession.IsValid())
|
|
{
|
|
ActiveSessionTraceDescriptor = TraceDescriptor;
|
|
}
|
|
}
|
|
|
|
return ActiveSessionTraceDescriptor.IsValid();
|
|
}
|
|
|
|
void FStateTreeDebugger::SetScrubStateCollectionIndex(const int32 EventCollectionIndex)
|
|
{
|
|
ScrubState.SetEventCollectionIndex(EventCollectionIndex);
|
|
|
|
OnScrubStateChanged.ExecuteIfBound(ScrubState);
|
|
|
|
RefreshActiveStates();
|
|
}
|
|
|
|
void FStateTreeDebugger::GetLiveTraces(TArray<FTraceDescriptor>& OutTraceDescriptors) const
|
|
{
|
|
UE::Trace::FStoreClient* StoreClient = GetStoreClient();
|
|
if (StoreClient == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
OutTraceDescriptors.Reset();
|
|
|
|
const uint32 SessionCount = StoreClient->GetSessionCount();
|
|
for (uint32 SessionIndex = 0; SessionIndex < SessionCount; ++SessionIndex)
|
|
{
|
|
const UE::Trace::FStoreClient::FSessionInfo* SessionInfo = StoreClient->GetSessionInfo(SessionIndex);
|
|
if (SessionInfo != nullptr)
|
|
{
|
|
const uint32 TraceId = SessionInfo->GetTraceId();
|
|
const UE::Trace::FStoreClient::FTraceInfo* TraceInfo = StoreClient->GetTraceInfoById(TraceId);
|
|
if (TraceInfo != nullptr)
|
|
{
|
|
FTraceDescriptor& Trace = OutTraceDescriptors.AddDefaulted_GetRef();
|
|
Trace.TraceId = TraceId;
|
|
Trace.Name = FString(TraceInfo->GetName());
|
|
UpdateMetadata(Trace);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FStateTreeDebugger::UpdateMetadata(FTraceDescriptor& TraceDescriptor) const
|
|
{
|
|
UE::Trace::FStoreClient* StoreClient = GetStoreClient();
|
|
if (StoreClient == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const UE::Trace::FStoreClient::FTraceData TraceData = StoreClient->ReadTrace(TraceDescriptor.TraceId);
|
|
if (!TraceData)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// inspired from FStoreBrowser
|
|
struct FDataStream : public UE::Trace::IInDataStream
|
|
{
|
|
enum class EReadStatus
|
|
{
|
|
Ready = 0,
|
|
StoppedByReadSizeLimit
|
|
};
|
|
|
|
virtual int32 Read(void* Data, const uint32 Size) override
|
|
{
|
|
if (BytesRead >= 1024 * 1024)
|
|
{
|
|
Status = EReadStatus::StoppedByReadSizeLimit;
|
|
return 0;
|
|
}
|
|
const int32 InnerBytesRead = Inner->Read(Data, Size);
|
|
BytesRead += InnerBytesRead;
|
|
|
|
return InnerBytesRead;
|
|
}
|
|
|
|
virtual void Close() override
|
|
{
|
|
Inner->Close();
|
|
}
|
|
|
|
IInDataStream* Inner = nullptr;
|
|
int32 BytesRead = 0;
|
|
EReadStatus Status = EReadStatus::Ready;
|
|
};
|
|
|
|
FDataStream DataStream;
|
|
DataStream.Inner = TraceData.Get();
|
|
|
|
UE::StateTreeDebugger::FDiagnosticsSessionAnalyzer Analyzer;
|
|
UE::Trace::FAnalysisContext Context;
|
|
Context.AddAnalyzer(Analyzer);
|
|
Context.Process(DataStream).Wait();
|
|
|
|
TraceDescriptor.SessionInfo = Analyzer.SessionInfo;
|
|
}
|
|
|
|
FText FStateTreeDebugger::GetSelectedTraceDescription() const
|
|
{
|
|
if (ActiveSessionTraceDescriptor.IsValid())
|
|
{
|
|
return DescribeTrace(ActiveSessionTraceDescriptor);
|
|
}
|
|
|
|
return LOCTEXT("NoSelectedTraceDescriptor", "No trace selected");
|
|
}
|
|
|
|
void FStateTreeDebugger::SetScrubTime(const double ScrubTime)
|
|
{
|
|
if (ScrubState.SetScrubTime(ScrubTime))
|
|
{
|
|
OnScrubStateChanged.ExecuteIfBound(ScrubState);
|
|
|
|
RefreshActiveStates();
|
|
}
|
|
}
|
|
|
|
bool FStateTreeDebugger::IsActiveInstance(const double Time, const FStateTreeInstanceDebugId InstanceId) const
|
|
{
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
|
|
if (const IStateTreeTraceProvider* Provider = Session->ReadProvider<IStateTreeTraceProvider>(FStateTreeTraceProvider::ProviderName))
|
|
{
|
|
const TSharedPtr<const UE::StateTreeDebugger::FInstanceDescriptor> Descriptor = Provider->GetInstanceDescriptor(InstanceId);
|
|
return Descriptor.IsValid() && Descriptor->Lifetime.Contains(Time);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
FText FStateTreeDebugger::DescribeTrace(const FTraceDescriptor& TraceDescriptor)
|
|
{
|
|
if (TraceDescriptor.IsValid())
|
|
{
|
|
const TraceServices::FSessionInfo& SessionInfo = TraceDescriptor.SessionInfo;
|
|
|
|
return FText::FromString(FString::Printf(TEXT("%s-%s-%s-%s-%s"),
|
|
*LexToString(TraceDescriptor.TraceId),
|
|
*SessionInfo.Platform,
|
|
*SessionInfo.AppName,
|
|
LexToString(SessionInfo.ConfigurationType),
|
|
LexToString(SessionInfo.TargetType)));
|
|
}
|
|
|
|
return LOCTEXT("InvalidTraceDescriptor", "Invalid");
|
|
}
|
|
|
|
FText FStateTreeDebugger::DescribeInstance(const UE::StateTreeDebugger::FInstanceDescriptor& InstanceDesc)
|
|
{
|
|
if (InstanceDesc.IsValid() == false)
|
|
{
|
|
return LOCTEXT("NoSelectedInstanceDescriptor", "No instance selected");
|
|
}
|
|
return FText::FromString(LexToString(InstanceDesc));
|
|
}
|
|
|
|
void FStateTreeDebugger::SetActiveStates(const FStateTreeTraceActiveStates& NewActiveStates)
|
|
{
|
|
ActiveStates = NewActiveStates;
|
|
OnActiveStatesChanged.ExecuteIfBound(ActiveStates);
|
|
}
|
|
|
|
void FStateTreeDebugger::RefreshActiveStates()
|
|
{
|
|
if (ScrubState.IsPointingToValidActiveStates())
|
|
{
|
|
const UE::StateTreeDebugger::FInstanceEventCollection& EventCollection = EventCollections[ScrubState.GetEventCollectionIndex()];
|
|
const int32 EventIndex = EventCollection.ActiveStatesChanges[ScrubState.GetActiveStatesIndex()].EventIndex;
|
|
SetActiveStates(EventCollection.Events[EventIndex].Get<FStateTreeTraceActiveStatesEvent>().ActiveStates);
|
|
}
|
|
else
|
|
{
|
|
SetActiveStates(FStateTreeTraceActiveStates());
|
|
}
|
|
}
|
|
|
|
bool FStateTreeDebugger::CanStepBackToPreviousStateWithEvents() const
|
|
{
|
|
return ScrubState.HasPreviousFrame();
|
|
}
|
|
|
|
void FStateTreeDebugger::StepBackToPreviousStateWithEvents()
|
|
{
|
|
ScrubState.GotoPreviousFrame();
|
|
OnScrubStateChanged.Execute(ScrubState);
|
|
|
|
RefreshActiveStates();
|
|
}
|
|
|
|
bool FStateTreeDebugger::CanStepForwardToNextStateWithEvents() const
|
|
{
|
|
return ScrubState.HasNextFrame();
|
|
}
|
|
|
|
void FStateTreeDebugger::StepForwardToNextStateWithEvents()
|
|
{
|
|
ScrubState.GotoNextFrame();
|
|
OnScrubStateChanged.Execute(ScrubState);
|
|
|
|
RefreshActiveStates();
|
|
}
|
|
|
|
bool FStateTreeDebugger::CanStepBackToPreviousStateChange() const
|
|
{
|
|
return ScrubState.HasPreviousActiveStates();
|
|
}
|
|
|
|
void FStateTreeDebugger::StepBackToPreviousStateChange()
|
|
{
|
|
ScrubState.GotoPreviousActiveStates();
|
|
OnScrubStateChanged.Execute(ScrubState);
|
|
|
|
RefreshActiveStates();
|
|
}
|
|
|
|
bool FStateTreeDebugger::CanStepForwardToNextStateChange() const
|
|
{
|
|
return ScrubState.HasNextActiveStates();
|
|
}
|
|
|
|
void FStateTreeDebugger::StepForwardToNextStateChange()
|
|
{
|
|
ScrubState.GotoNextActiveStates();
|
|
OnScrubStateChanged.Execute(ScrubState);
|
|
|
|
RefreshActiveStates();
|
|
}
|
|
|
|
bool FStateTreeDebugger::HasStateBreakpoint(const FStateTreeStateHandle StateHandle, const EStateTreeBreakpointType BreakpointType) const
|
|
{
|
|
return Breakpoints.ContainsByPredicate([StateHandle, BreakpointType](const FStateTreeDebuggerBreakpoint Breakpoint)
|
|
{
|
|
if (Breakpoint.BreakpointType == BreakpointType)
|
|
{
|
|
const FStateTreeStateHandle* BreakpointStateHandle = Breakpoint.ElementIdentifier.TryGet<FStateTreeStateHandle>();
|
|
return (BreakpointStateHandle != nullptr && *BreakpointStateHandle == StateHandle);
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
bool FStateTreeDebugger::HasTaskBreakpoint(const FStateTreeIndex16 Index, const EStateTreeBreakpointType BreakpointType) const
|
|
{
|
|
return Breakpoints.ContainsByPredicate([Index, BreakpointType](const FStateTreeDebuggerBreakpoint Breakpoint)
|
|
{
|
|
if (Breakpoint.BreakpointType == BreakpointType)
|
|
{
|
|
const FStateTreeDebuggerBreakpoint::FStateTreeTaskIndex* BreakpointTaskIndex = Breakpoint.ElementIdentifier.TryGet<FStateTreeDebuggerBreakpoint::FStateTreeTaskIndex>();
|
|
return (BreakpointTaskIndex != nullptr && BreakpointTaskIndex->Index == Index);
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
bool FStateTreeDebugger::HasTransitionBreakpoint(const FStateTreeIndex16 Index, const EStateTreeBreakpointType BreakpointType) const
|
|
{
|
|
return Breakpoints.ContainsByPredicate([Index, BreakpointType](const FStateTreeDebuggerBreakpoint Breakpoint)
|
|
{
|
|
if (Breakpoint.BreakpointType == BreakpointType)
|
|
{
|
|
const FStateTreeDebuggerBreakpoint::FStateTreeTransitionIndex* BreakpointTransitionIndex = Breakpoint.ElementIdentifier.TryGet<FStateTreeDebuggerBreakpoint::FStateTreeTransitionIndex>();
|
|
return (BreakpointTransitionIndex != nullptr && BreakpointTransitionIndex->Index == Index);
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
void FStateTreeDebugger::SetStateBreakpoint(const FStateTreeStateHandle StateHandle, const EStateTreeBreakpointType BreakpointType)
|
|
{
|
|
Breakpoints.Emplace(StateHandle, BreakpointType);
|
|
}
|
|
|
|
void FStateTreeDebugger::SetTransitionBreakpoint(const FStateTreeIndex16 TransitionIndex, const EStateTreeBreakpointType BreakpointType)
|
|
{
|
|
Breakpoints.Emplace(FStateTreeDebuggerBreakpoint::FStateTreeTransitionIndex(TransitionIndex), BreakpointType);
|
|
}
|
|
|
|
void FStateTreeDebugger::SetTaskBreakpoint(const FStateTreeIndex16 NodeIndex, const EStateTreeBreakpointType BreakpointType)
|
|
{
|
|
Breakpoints.Emplace(FStateTreeDebuggerBreakpoint::FStateTreeTaskIndex(NodeIndex), BreakpointType);
|
|
}
|
|
|
|
void FStateTreeDebugger::ClearBreakpoint(const FStateTreeIndex16 NodeIndex, const EStateTreeBreakpointType BreakpointType)
|
|
{
|
|
const int32 Index = Breakpoints.IndexOfByPredicate([NodeIndex, BreakpointType](const FStateTreeDebuggerBreakpoint& Breakpoint)
|
|
{
|
|
const FStateTreeDebuggerBreakpoint::FStateTreeTaskIndex* IndexPtr = Breakpoint.ElementIdentifier.TryGet<FStateTreeDebuggerBreakpoint::FStateTreeTaskIndex>();
|
|
return (IndexPtr != nullptr && IndexPtr->Index == NodeIndex && Breakpoint.BreakpointType == BreakpointType);
|
|
});
|
|
|
|
if (Index != INDEX_NONE)
|
|
{
|
|
Breakpoints.RemoveAtSwap(Index);
|
|
}
|
|
}
|
|
|
|
void FStateTreeDebugger::ClearAllBreakpoints()
|
|
{
|
|
Breakpoints.Empty();
|
|
}
|
|
|
|
const TraceServices::IAnalysisSession* FStateTreeDebugger::GetAnalysisSession() const
|
|
{
|
|
return AnalysisSession.Get();
|
|
}
|
|
|
|
bool FStateTreeDebugger::RequestSessionAnalysis(const FTraceDescriptor& TraceDescriptor)
|
|
{
|
|
if (StartSessionAnalysis(TraceDescriptor))
|
|
{
|
|
UpdateAnalysisTransitionType(EAnalysisSourceType::SelectedSession);
|
|
|
|
SetScrubStateCollectionIndex(INDEX_NONE);
|
|
OnNewSession.ExecuteIfBound();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FStateTreeDebugger::UpdateAnalysisTransitionType(const EAnalysisSourceType SourceType)
|
|
{
|
|
switch (AnalysisTransitionType)
|
|
{
|
|
case EAnalysisTransitionType::Unset:
|
|
AnalysisTransitionType = (SourceType == EAnalysisSourceType::SelectedSession)
|
|
? EAnalysisTransitionType::NoneToSelected
|
|
: EAnalysisTransitionType::NoneToEditor;
|
|
break;
|
|
|
|
case EAnalysisTransitionType::NoneToSelected:
|
|
case EAnalysisTransitionType::EditorToSelected:
|
|
case EAnalysisTransitionType::SelectedToSelected:
|
|
AnalysisTransitionType = (SourceType == EAnalysisSourceType::SelectedSession)
|
|
? EAnalysisTransitionType::SelectedToSelected
|
|
: EAnalysisTransitionType::SelectedToEditor;
|
|
break;
|
|
|
|
case EAnalysisTransitionType::NoneToEditor:
|
|
case EAnalysisTransitionType::EditorToEditor:
|
|
case EAnalysisTransitionType::SelectedToEditor:
|
|
AnalysisTransitionType = (SourceType == EAnalysisSourceType::SelectedSession)
|
|
? EAnalysisTransitionType::EditorToSelected
|
|
: EAnalysisTransitionType::EditorToEditor;
|
|
break;
|
|
default:
|
|
ensureMsgf(false, TEXT("Unhandled transition type."));
|
|
}
|
|
}
|
|
|
|
UE::Trace::FStoreClient* FStateTreeDebugger::GetStoreClient() const
|
|
{
|
|
return StateTreeModule.GetStoreClient();
|
|
}
|
|
|
|
void FStateTreeDebugger::ReadTrace(const uint64 FrameIndex)
|
|
{
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
|
|
const TraceServices::IFrameProvider& FrameProvider = ReadFrameProvider(*Session);
|
|
|
|
if (const TraceServices::FFrame* TargetFrame = FrameProvider.GetFrame(TraceFrameType_Game, FrameIndex))
|
|
{
|
|
ReadTrace(*Session, FrameProvider, *TargetFrame);
|
|
}
|
|
}
|
|
|
|
// Notify outside session read scope
|
|
SendNotifications();
|
|
}
|
|
|
|
void FStateTreeDebugger::ReadTrace(const double ScrubTime)
|
|
{
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
|
|
const TraceServices::IFrameProvider& FrameProvider = ReadFrameProvider(*Session);
|
|
|
|
TraceServices::FFrame TargetFrame;
|
|
if (FrameProvider.GetFrameFromTime(TraceFrameType_Game, ScrubTime, TargetFrame))
|
|
{
|
|
// Process only completed frames
|
|
if (TargetFrame.EndTime == std::numeric_limits<double>::infinity())
|
|
{
|
|
if (const TraceServices::FFrame* PreviousCompleteFrame = FrameProvider.GetFrame(TraceFrameType_Game, TargetFrame.Index - 1))
|
|
{
|
|
ReadTrace(*Session, FrameProvider, *PreviousCompleteFrame);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ReadTrace(*Session, FrameProvider, TargetFrame);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notify outside session read scope
|
|
SendNotifications();
|
|
}
|
|
|
|
void FStateTreeDebugger::SendNotifications()
|
|
{
|
|
if (NewInstances.Num() > 0)
|
|
{
|
|
for (const FStateTreeInstanceDebugId NewInstanceId : NewInstances)
|
|
{
|
|
OnNewInstance.ExecuteIfBound(NewInstanceId);
|
|
}
|
|
NewInstances.Reset();
|
|
}
|
|
|
|
if (HitBreakpoint.IsSet())
|
|
{
|
|
check(HitBreakpoint.InstanceId.IsValid());
|
|
check(Breakpoints.IsValidIndex(HitBreakpoint.Index));
|
|
|
|
// Force scrub time to latest simulation time to reflect most recent events.
|
|
// This will notify scrub position changed and active states
|
|
SetScrubTime(HitBreakpoint.Time);
|
|
|
|
// Make sure the instance is selected in case the breakpoint was set for any instances
|
|
if (SelectedInstanceId != HitBreakpoint.InstanceId)
|
|
{
|
|
SelectInstance(HitBreakpoint.InstanceId);
|
|
}
|
|
|
|
OnBreakpointHit.ExecuteIfBound(HitBreakpoint.InstanceId, Breakpoints[HitBreakpoint.Index]);
|
|
|
|
PauseSessionAnalysis();
|
|
}
|
|
}
|
|
|
|
void FStateTreeDebugger::ReadTrace(
|
|
const TraceServices::IAnalysisSession& Session,
|
|
const TraceServices::IFrameProvider& FrameProvider,
|
|
const TraceServices::FFrame& Frame
|
|
)
|
|
{
|
|
TraceServices::FFrame LastReadFrame;
|
|
const bool bValidLastReadFrame = FrameProvider.GetFrameFromTime(TraceFrameType_Game, LastTraceReadTime, LastReadFrame);
|
|
if (LastTraceReadTime == 0 || (bValidLastReadFrame && Frame.Index > LastReadFrame.Index))
|
|
{
|
|
if (const IStateTreeTraceProvider* Provider = Session.ReadProvider<IStateTreeTraceProvider>(FStateTreeTraceProvider::ProviderName))
|
|
{
|
|
AddEvents(LastTraceReadTime, Frame.EndTime, FrameProvider, *Provider);
|
|
LastTraceReadTime = Frame.EndTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FStateTreeDebugger::EvaluateBreakpoints(const FStateTreeInstanceDebugId InstanceId, const FStateTreeTraceEventVariantType& Event)
|
|
{
|
|
if (StateTreeAsset == nullptr // asset is required to properly match state handles
|
|
|| HitBreakpoint.IsSet() // Only consider first hit breakpoint in the frame
|
|
|| Breakpoints.IsEmpty()
|
|
|| (SelectedInstanceId.IsValid() && InstanceId != SelectedInstanceId)) // ignore events not for the selected instances
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (int BreakpointIndex = 0; BreakpointIndex < Breakpoints.Num(); ++BreakpointIndex)
|
|
{
|
|
const FStateTreeDebuggerBreakpoint Breakpoint = Breakpoints[BreakpointIndex];
|
|
if (Breakpoint.IsMatchingEvent(Event))
|
|
{
|
|
HitBreakpoint.Index = BreakpointIndex;
|
|
HitBreakpoint.InstanceId = InstanceId;
|
|
HitBreakpoint.Time = RecordingDuration;
|
|
}
|
|
}
|
|
|
|
return HitBreakpoint.IsSet();
|
|
}
|
|
|
|
bool FStateTreeDebugger::ProcessEvent(const FStateTreeInstanceDebugId InstanceId, const TraceServices::FFrame& Frame, const FStateTreeTraceEventVariantType& Event)
|
|
{
|
|
UE::StateTreeDebugger::FInstanceEventCollection* ExistingCollection = EventCollections.FindByPredicate(
|
|
[InstanceId](const UE::StateTreeDebugger::FInstanceEventCollection& Entry)
|
|
{
|
|
return Entry.InstanceId == InstanceId;
|
|
});
|
|
|
|
// Create missing EventCollection if necessary
|
|
if (ExistingCollection == nullptr)
|
|
{
|
|
// Push deferred notification for new instance Id
|
|
NewInstances.Push(InstanceId);
|
|
|
|
ExistingCollection = &EventCollections.Emplace_GetRef(InstanceId);
|
|
|
|
// Update the active event collection index when it's newly created for the currently debugged instance.
|
|
// Otherwise (i.e. EventCollection already exists) it is updated when switching instance (i.e. SelectInstance)
|
|
if (SelectedInstanceId == InstanceId && ScrubState.GetEventCollectionIndex() == INDEX_NONE)
|
|
{
|
|
SetScrubStateCollectionIndex(EventCollections.Num()-1);
|
|
}
|
|
}
|
|
|
|
check(ExistingCollection);
|
|
TArray<FStateTreeTraceEventVariantType>& Events = ExistingCollection->Events;
|
|
|
|
TraceServices::FFrame FrameToAddInSpans = Frame;
|
|
bool bShouldAddFrameToSpans = false;
|
|
|
|
double RecordingWorldTime = 0;
|
|
Visit([&RecordingWorldTime](auto& TypedEvent)
|
|
{
|
|
RecordingWorldTime = TypedEvent.RecordingWorldTime;
|
|
}, Event);
|
|
|
|
// Add new frame span if none added yet
|
|
if (ExistingCollection->FrameSpans.IsEmpty())
|
|
{
|
|
bShouldAddFrameToSpans = true;
|
|
}
|
|
else
|
|
{
|
|
const UE::StateTreeDebugger::FFrameSpan& LastSpan = ExistingCollection->FrameSpans.Last();
|
|
const TraceServices::FFrame& LastFrame = LastSpan.Frame;
|
|
const uint64 FrameIndexOffset = ExistingCollection->ContiguousTracesData.IsEmpty()
|
|
? 0
|
|
: (ExistingCollection->FrameSpans[ExistingCollection->ContiguousTracesData.Last().LastSpanIndex].Frame.Index + 1);
|
|
|
|
// Add new frame span for new larger frame index
|
|
if (Frame.Index + FrameIndexOffset > LastFrame.Index)
|
|
{
|
|
bShouldAddFrameToSpans = true;
|
|
|
|
// Apply current offset to the frame index
|
|
FrameToAddInSpans.Index += FrameIndexOffset;
|
|
}
|
|
else if (Frame.Index < LastFrame.Index && Frame.StartTime > LastFrame.StartTime)
|
|
{
|
|
// Frame index will restart at 0 if a new session is started,
|
|
// in that case we offset the frame we store to append to existing data
|
|
bShouldAddFrameToSpans = true;
|
|
|
|
const UE::StateTreeDebugger::FInstanceEventCollection::FContiguousTraceInfo& TraceInfo =
|
|
ExistingCollection->ContiguousTracesData.Emplace_GetRef(
|
|
UE::StateTreeDebugger::FInstanceEventCollection::FContiguousTraceInfo(ExistingCollection->FrameSpans.Num()-1));
|
|
FrameToAddInSpans.Index += ExistingCollection->FrameSpans[TraceInfo.LastSpanIndex].Frame.Index + 1;
|
|
}
|
|
}
|
|
|
|
if (bShouldAddFrameToSpans)
|
|
{
|
|
// Update global recording duration
|
|
RecordingDuration = RecordingWorldTime;
|
|
ExistingCollection->FrameSpans.Add(UE::StateTreeDebugger::FFrameSpan(FrameToAddInSpans, RecordingWorldTime, Events.Num()));
|
|
}
|
|
|
|
// Add activate states change info
|
|
if (Event.IsType<FStateTreeTraceActiveStatesEvent>())
|
|
{
|
|
checkf(ExistingCollection->FrameSpans.Num() > 0, TEXT("Expecting to always be in a frame span at this point."));
|
|
const int32 FrameSpanIndex = ExistingCollection->FrameSpans.Num()-1;
|
|
|
|
// Add new entry for the first event or if the last event is for a different frame
|
|
if (ExistingCollection->ActiveStatesChanges.IsEmpty()
|
|
|| ExistingCollection->ActiveStatesChanges.Last().SpanIndex != FrameSpanIndex)
|
|
{
|
|
ExistingCollection->ActiveStatesChanges.Push({FrameSpanIndex, Events.Num()});
|
|
}
|
|
else
|
|
{
|
|
// Multiple events for change of active states in the same frame, keep the last one until we implement scrubbing within a frame
|
|
ExistingCollection->ActiveStatesChanges.Last().EventIndex = Events.Num();
|
|
}
|
|
}
|
|
|
|
// Store event in the collection
|
|
Events.Emplace(Event);
|
|
|
|
// Process at the end so RecordingDuration is up to date and we can associate it to a hit breakpoint if necessary.
|
|
EvaluateBreakpoints(InstanceId, Event);
|
|
|
|
return /*bKeepProcessing*/true;
|
|
}
|
|
|
|
const UE::StateTreeDebugger::FInstanceEventCollection& FStateTreeDebugger::GetEventCollection(FStateTreeInstanceDebugId InstanceId) const\
|
|
{
|
|
using namespace UE::StateTreeDebugger;
|
|
const FInstanceEventCollection* ExistingCollection = EventCollections.FindByPredicate([InstanceId](const FInstanceEventCollection& Entry)
|
|
{
|
|
return Entry.InstanceId == InstanceId;
|
|
});
|
|
|
|
return ExistingCollection != nullptr ? *ExistingCollection : FInstanceEventCollection::Invalid;
|
|
}
|
|
|
|
void FStateTreeDebugger::ResetEventCollections()
|
|
{
|
|
EventCollections.Reset();
|
|
SetScrubStateCollectionIndex(INDEX_NONE);
|
|
RecordingDuration = 0;
|
|
}
|
|
|
|
void FStateTreeDebugger::AddEvents(const double StartTime, const double EndTime, const TraceServices::IFrameProvider& FrameProvider, const IStateTreeTraceProvider& StateTreeTraceProvider)
|
|
{
|
|
check(StateTreeAsset.IsValid());
|
|
StateTreeTraceProvider.ReadTimelines(*StateTreeAsset,
|
|
[this, StartTime, EndTime, &FrameProvider](const FStateTreeInstanceDebugId InstanceId, const IStateTreeTraceProvider::FEventsTimeline& TimelineData)
|
|
{
|
|
// Keep track of the frames containing events. Starting with an invalid frame.
|
|
TraceServices::FFrame Frame;
|
|
Frame.Index = INDEX_NONE;
|
|
|
|
TimelineData.EnumerateEvents(StartTime, EndTime,
|
|
[this, InstanceId, &FrameProvider, &Frame](const double EventStartTime, const double EventEndTime, uint32 InDepth, const FStateTreeTraceEventVariantType& Event)
|
|
{
|
|
bool bValidFrame = true;
|
|
|
|
// Fetch frame when not set yet or if events no longer part of the current one
|
|
if (Frame.Index == INDEX_NONE ||
|
|
(EventEndTime < Frame.StartTime || Frame.EndTime < EventStartTime))
|
|
{
|
|
bValidFrame = FrameProvider.GetFrameFromTime(TraceFrameType_Game, EventStartTime, Frame);
|
|
|
|
if (bValidFrame == false)
|
|
{
|
|
// Edge case for events from a missing first complete frame.
|
|
// (i.e. FrameProvider didn't get BeginFrame event but StateTreeEvent were sent in that frame)
|
|
// Doing this will merge our two first frames of state tree events using the same recording world time
|
|
// but this should happen only for late start recording.
|
|
const TraceServices::FFrame* FirstFrame = FrameProvider.GetFrame(TraceFrameType_Game, 0);
|
|
if (FirstFrame != nullptr && EventEndTime < FirstFrame->StartTime)
|
|
{
|
|
Frame = *FirstFrame;
|
|
bValidFrame = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bValidFrame)
|
|
{
|
|
const bool bKeepProcessing = ProcessEvent(InstanceId, Frame, Event);
|
|
return bKeepProcessing ? TraceServices::EEventEnumerate::Continue : TraceServices::EEventEnumerate::Stop;
|
|
}
|
|
|
|
// Skip events outside of game frames
|
|
return TraceServices::EEventEnumerate::Continue;
|
|
});
|
|
});
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|
|
|
|
#endif // WITH_STATETREE_TRACE_DEBUGGER
|