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

426 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#if WITH_STATETREE_TRACE_DEBUGGER
#include "Debugger/SStateTreeFrameEventsView.h"
#include "Debugger/StateTreeDebuggerTypes.h"
#include "SStateTreeDebuggerViewRow.h"
#include "UObject/Package.h"
#include "Widgets/Layout/SScrollBar.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Views/STableViewBase.h"
#include "Widgets/Views/STreeView.h"
namespace UE::StateTreeDebugger
{
/**
* Iterates over all tree elements for the frame events
* @param Elements Container of hierarchical tree element to visit
* @param InFunc function called at each element, should return true if visiting is continued or false to stop.
*/
void VisitEventTreeElements(const TConstArrayView<TSharedPtr<FFrameEventTreeElement>> Elements
, TFunctionRef<bool(TSharedPtr<FFrameEventTreeElement>& VisitedElement)> InFunc)
{
TArray<TSharedPtr<FFrameEventTreeElement>> Stack;
bool bContinue = true;
for (const TSharedPtr<FFrameEventTreeElement>& RootElement : Elements)
{
if (RootElement == nullptr)
{
continue;
}
Stack.Add(RootElement);
while (!Stack.IsEmpty() && bContinue)
{
TSharedPtr<FFrameEventTreeElement> StackedElement = Stack[0];
check(StackedElement);
Stack.RemoveAt(0);
bContinue = InFunc(StackedElement);
if (bContinue)
{
for (const TSharedPtr<FFrameEventTreeElement>& Child : StackedElement->Children)
{
if (Child.IsValid())
{
Stack.Add(Child);
}
}
}
}
if (!bContinue)
{
break;
}
}
}
//----------------------------------------------------------------------//
// SFrameEventsView
//----------------------------------------------------------------------//
void SFrameEventsView::Construct(const FArguments& InArgs, TNotNull<const UStateTree*> InStateTree)
{
WeakStateTree = InStateTree;
// EventsTreeView scrollbars
TSharedRef<SScrollBar> HorizontalScrollBar = SNew(SScrollBar)
.Orientation(Orient_Horizontal)
.Thickness(FVector2D(12.0f, 12.0f));
TSharedRef<SScrollBar> VerticalScrollBar = SNew(SScrollBar)
.Orientation(Orient_Vertical)
.Thickness(FVector2D(12.0f, 12.0f));
// EventsTreeView
EventsTreeView = SNew(STreeView<TSharedPtr<FFrameEventTreeElement>>)
.OnGenerateRow_Lambda([this](const TSharedPtr<FFrameEventTreeElement>& InElement, const TSharedRef<STableViewBase>& InOwnerTableView)
{
return SNew(SFrameEventViewRow, InOwnerTableView, InElement);
})
.OnGetChildren_Lambda([](const TSharedPtr<const FFrameEventTreeElement>& InParent, TArray<TSharedPtr<FFrameEventTreeElement>>& OutChildren)
{
if (const FFrameEventTreeElement* Parent = InParent.Get())
{
OutChildren.Append(Parent->Children);
}
})
.TreeItemsSource(&EventsTreeElements)
.AllowOverscroll(EAllowOverscroll::No)
.ExternalScrollbar(VerticalScrollBar);
ChildSlot
[
SNew(SBorder)
.Padding(4.0f)
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
[
SNew(SSplitter)
.Orientation(Orient_Horizontal)
+ SSplitter::Slot()
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(0.0f)
[
SNew(SScrollBox)
.Orientation(Orient_Horizontal)
.ExternalScrollbar(HorizontalScrollBar)
+ SScrollBox::Slot()
.FillSize(1.0f)
[
EventsTreeView.ToSharedRef()
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
VerticalScrollBar
]
]
+ SVerticalBox::Slot()
.AutoHeight()
[
HorizontalScrollBar
]
]
]
]
];
}
void SFrameEventsView::SelectByPredicate(TFunctionRef<bool(const FStateTreeTraceEventVariantType& Event)> InPredicate)
{
TSharedPtr<FFrameEventTreeElement> MatchingElement = nullptr;
VisitEventTreeElements(EventsTreeElements, [&MatchingElement, InPredicate](const TSharedPtr<FFrameEventTreeElement>& VisitedElement)
{
if (InPredicate(VisitedElement->Event))
{
MatchingElement = VisitedElement;
}
// Continue visit until we find a matching event
return MatchingElement.IsValid() == false;
});
if (MatchingElement.IsValid())
{
EventsTreeView->SetSelection(MatchingElement);
}
}
void SFrameEventsView::GenerateElementsForProperties(const FStateTreeTraceEventVariantType& Event, const TSharedRef<FFrameEventTreeElement>& ParentElement)
{
FString TypePath;
FString InstanceDataAsString;
FString DebugText;
Visit([&TypePath, &InstanceDataAsString, &DebugText](auto& TypedEvent)
{
TypePath = TypedEvent.GetDataTypePath();
InstanceDataAsString = TypedEvent.GetDataAsText();
DebugText = TypedEvent.GetDebugText();
}, Event);
auto CreateTreeElement = [ParentElement]<typename EventType>(const FString& Line)
{
// Create new event
EventType Event(/*RecordingWorldTime*/0, ELogVerbosity::Verbose, *Line);
// Create Tree element to hold the event
const TSharedPtr<FFrameEventTreeElement> NewChildElement = MakeShareable(new FFrameEventTreeElement(
ParentElement->Frame,
FStateTreeTraceEventVariantType(TInPlaceType<EventType>(), Event),
ParentElement->WeakStateTree.Get()));
ParentElement->Children.Add(NewChildElement);
};
if (!InstanceDataAsString.IsEmpty())
{
auto CreatePropertyElement = [&CreateTreeElement](const FStringView Line, const int32 NestedCount = 0)
{
constexpr int32 Indent = 4;
FString ConvertedString = FString::Printf(TEXT("%*s"), NestedCount * Indent, TEXT(""));
ConvertedString.Append(Line);
ConvertedString.ReplaceInline(TEXT("="), TEXT(" = "));
ConvertedString.ReplaceInline(TEXT("\""), TEXT(""));
CreateTreeElement.operator()<FStateTreeTracePropertyEvent>(ConvertedString);
};
// Try to parse Struct for which properties are exported between '(' and ')'
if (InstanceDataAsString.StartsWith("(") && InstanceDataAsString.EndsWith(")"))
{
const FStringView View(GetData(InstanceDataAsString) + 1, InstanceDataAsString.Len() - 2);
const TCHAR* ViewIt = View.GetData();
const TCHAR* const ViewEnd = ViewIt + View.Len();
const TCHAR* NextToken = ViewIt;
int32 NestedCount = 0;
for (; ViewIt != ViewEnd; ++ViewIt)
{
int32 LocalNestedCount = 0;
if (*ViewIt == TCHAR('('))
{
LocalNestedCount++;
}
else if (*ViewIt == TCHAR(')'))
{
LocalNestedCount--;
}
else if (*ViewIt != TCHAR(','))
{
continue;
}
if (ViewIt != NextToken)
{
CreatePropertyElement(FStringView(NextToken, UE_PTRDIFF_TO_INT32(ViewIt - NextToken)), NestedCount);
}
NextToken = ViewIt + 1;
NestedCount += LocalNestedCount;
}
if (ViewIt != NextToken)
{
CreatePropertyElement(FStringView(NextToken, UE_PTRDIFF_TO_INT32(ViewIt - NextToken)), NestedCount);
}
}
else
{
const TCHAR* Buffer = *InstanceDataAsString;
FParse::Next(&Buffer);
FString StrLine;
while (FParse::Line(&Buffer, StrLine))
{
const TCHAR* Str = *StrLine;
if (!FParse::Command(&Str, TEXT("BEGIN OBJECT"))
&& !FParse::Command(&Str, TEXT("END OBJECT")))
{
CreatePropertyElement(Str);
}
}
}
}
if (!DebugText.IsEmpty())
{
CreateTreeElement.operator()<FStateTreeTraceLogEvent>(DebugText);
}
}
void SFrameEventsView::ExpandAll(const TArray<TSharedPtr<FFrameEventTreeElement>>& Items)
{
for (const TSharedPtr<FFrameEventTreeElement>& Item : Items)
{
bool bExpand = true;
if (Item->Children.Num() > 0)
{
if (const FFrameEventTreeElement* FirstChild = Item->Children[0].Get())
{
if (const FStateTreeTraceLogEvent* LogEvent = FirstChild->Event.TryGet<FStateTreeTraceLogEvent>())
{
// Do not auto expand verbose logs
bExpand = (LogEvent->Verbosity < ELogVerbosity::Verbose);
}
else if (FirstChild->Event.IsType<FStateTreeTracePropertyEvent>())
{
bExpand = false;
}
}
}
if (bExpand)
{
EventsTreeView->SetItemExpansion(Item, true);
ExpandAll(Item->Children);
}
}
}
void SFrameEventsView::RequestRefresh(const FScrubState& InScrubState)
{
const FInstanceEventCollection& EventCollection = InScrubState.GetEventCollection();
if (EventCollection.IsInvalid())
{
return;
}
// Rebuild frame details from the events of that frame
EventsTreeElements.Reset();
ON_SCOPE_EXIT
{
EventsTreeView->ClearExpandedItems();
ExpandAll(EventsTreeElements);
EventsTreeView->RequestTreeRefresh();
};
const TConstArrayView<const FStateTreeTraceEventVariantType> Events = EventCollection.Events;
if (Events.IsEmpty()
|| !InScrubState.IsInBounds())
{
return;
}
const TConstArrayView<FFrameSpan> Spans = EventCollection.FrameSpans;
check(Spans.Num());
check(WeakStateTree.IsValid());
TArray<TSharedPtr<FFrameEventTreeElement>, TInlineAllocator<8>> ScopeStack;
const int32 SpanIdx = InScrubState.GetFrameSpanIndex();
if (!Spans.IsValidIndex(SpanIdx))
{
UE_LOG(LogStateTree, Error, TEXT("Invalid index in span: Idx: %i, Num Spans: %i"), SpanIdx, Spans.Num());
return;
}
const FFrameSpan& Span = Spans[SpanIdx];
if (InScrubState.GetScrubTime() < Span.GetWorldTimeStart()
|| InScrubState.GetScrubTime() > Span.GetWorldTimeEnd())
{
return;
}
const int32 FirstEventIdx = Span.EventIdx;
const TraceServices::FFrame Frame = Span.Frame;
const int32 MaxEventIdx = Spans.IsValidIndex(SpanIdx + 1) ? Spans[SpanIdx + 1].EventIdx : Events.Num();
const UStateTree* const RootTree = WeakStateTree.Get();
const UStateTree* ActiveTree = RootTree;
for (int32 EventIdx = FirstEventIdx; EventIdx < MaxEventIdx; EventIdx++)
{
const FStateTreeTraceEventVariantType& Event = Events[EventIdx];
FString CustomDescription;
bool bShouldAddToScopeStack = false;
bool bShouldPopScopeStack = false;
if (const FStateTreeTraceStateEvent* StateEvent = Event.TryGet<FStateTreeTraceStateEvent>())
{
if (StateEvent->EventType == EStateTreeTraceEventType::OnEntering
|| StateEvent->EventType == EStateTreeTraceEventType::OnExiting
|| StateEvent->EventType == EStateTreeTraceEventType::Push)
{
bShouldAddToScopeStack = true;
}
else if (StateEvent->EventType == EStateTreeTraceEventType::OnEntered
|| StateEvent->EventType == EStateTreeTraceEventType::OnExited
|| StateEvent->EventType == EStateTreeTraceEventType::Pop)
{
bShouldPopScopeStack = true;
}
}
else if (const FStateTreeTracePhaseEvent* PhaseEvent = Event.TryGet<FStateTreeTracePhaseEvent>())
{
if (PhaseEvent->EventType == EStateTreeTraceEventType::Push)
{
bShouldAddToScopeStack = true;
}
else if (PhaseEvent->EventType == EStateTreeTraceEventType::Pop)
{
bShouldPopScopeStack = true;
}
}
else if (const FStateTreeTraceInstanceFrameEvent* FrameEvent = Event.TryGet<FStateTreeTraceInstanceFrameEvent>())
{
ActiveTree = FrameEvent->WeakStateTree.Get();
check(ActiveTree);
// We don't want to create an entry.
continue;
}
if (bShouldPopScopeStack)
{
// Pop scope and remove associated element if empty
if (ensureMsgf(ScopeStack.Num() > 0, TEXT("Expected to pop an entry in the scope stack but it is already empty.")))
{
TSharedPtr<FFrameEventTreeElement> Scope = ScopeStack.Pop();
if (Scope->Children.IsEmpty())
{
TArray<TSharedPtr<FFrameEventTreeElement>>& TreeElements = ScopeStack.IsEmpty() ? EventsTreeElements : ScopeStack.Top()->Children;
TreeElements.Remove(Scope);
}
}
// We don't want to create a child when a scope is popped.
continue;
}
const TSharedRef<FFrameEventTreeElement> NewElement = MakeShareable(new FFrameEventTreeElement(Frame, Event, ActiveTree));
NewElement->Description = CustomDescription;
TArray<TSharedPtr<FFrameEventTreeElement>>& TreeElements = ScopeStack.IsEmpty() ? EventsTreeElements : ScopeStack.Top()->Children;
const TSharedPtr<FFrameEventTreeElement>& ElementPtr = TreeElements.Add_GetRef(NewElement);
if (bShouldAddToScopeStack)
{
ScopeStack.Push(ElementPtr);
}
GenerateElementsForProperties(Event, NewElement);
}
}
} // UE::StateTreeDebugger
#endif // WITH_STATETREE_TRACE_DEBUGGER