// 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("Changelist", 0); SessionInfo.ConfigurationType = (EBuildConfiguration) EventData.GetValue("ConfigurationType"); SessionInfo.TargetType = (EBuildTargetType) EventData.GetValue("TargetType"); return false; } default: ; } return true; } enum : uint16 { RouteId_Session2, }; TraceServices::FSessionInfo SessionInfo; }; } // UE::StateTreeDebugger //----------------------------------------------------------------// // FStateTreeDebugger //----------------------------------------------------------------// FStateTreeDebugger::FStateTreeDebugger() : StateTreeModule(FModuleManager::GetModuleChecked("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(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& OutInstances) const { PRAGMA_DISABLE_DEPRECATION_WARNINGS if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession()) { TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session); if (const IStateTreeTraceProvider* Provider = Session->ReadProvider(FStateTreeTraceProvider::ProviderName)) { Provider->GetInstances(OutInstances); } } PRAGMA_ENABLE_DEPRECATION_WARNINGS } void FStateTreeDebugger::GetSessionInstanceDescriptors(TArray>& OutInstances) const { if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession()) { TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session); if (const IStateTreeTraceProvider* Provider = Session->ReadProvider(FStateTreeTraceProvider::ProviderName)) { Provider->GetInstances(OutInstances); } } } bool FStateTreeDebugger::RequestAnalysisOfEditorSession() { // Get snapshot of current trace to help identify the next live one TArray 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 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("TraceServices"); if (const TSharedPtr 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& 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(FStateTreeTraceProvider::ProviderName)) { const TSharedPtr 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().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(); 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(); 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(); 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(); 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::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(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& 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()) { 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