// Copyright Epic Games, Inc. All Rights Reserved. #include "SEventsTree.h" #include "SlateOptMacros.h" #include "Textures/SlateIcon.h" #include "Framework/Commands/UIAction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SSearchBox.h" #define LOCTEXT_NAMESPACE "SEventsTree" FName SEventsTree::NAME_NameColumn = FName(*NSLOCTEXT("ProfileVisualizer", "ColumnName", "Name").ToString()); FName SEventsTree::NAME_DurationColumn = FName( *NSLOCTEXT("ProfileVisualizer", "ColumnDuration", "Duration").ToString() ); void SEventItem::Construct( const FArguments& InArgs, const TSharedRef& InOwnerTableView ) { EventName = InArgs._EventName; EventDuration = InArgs._EventDuration; SMultiColumnTableRow< TSharedPtr< FVisualizerEvent > >::Construct( FSuperRowType::FArguments(), InOwnerTableView ); } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION TSharedRef< SWidget > SEventItem::GenerateWidgetForColumn( const FName& ColumnName ) { if( ColumnName == SEventsTree::NAME_NameColumn ) { return SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew( SExpanderArrow, SharedThis(this) ) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew( STextBlock ) .Text( FText::FromString(EventName) ) ]; } else if( ColumnName == SEventsTree::NAME_DurationColumn ) { return SNew( STextBlock ) .Text( this, &SEventItem::GetDurationText ); } else { return SNew( STextBlock ) . Text(FText::Format( LOCTEXT("UnsupportedColumnFmt", "Unsupported Column: {0}"), FText::FromName(ColumnName) ) ); } } END_SLATE_FUNCTION_BUILD_OPTIMIZATION ///////////////////////////////////////////////////////////////////////////////////////////////////////////// BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SEventsTree::Construct( const FArguments& InArgs ) { DurationUnits = EVisualizerTimeUnits::Milliseconds; ViewMode = EVisualizerViewMode::Hierarchical; bSuppressSelectionChangedEvent = false; OnEventSelectionChangedDelegate = InArgs._OnEventSelectionChanged; ProfileData = InArgs._ProfileData.Get(); // Duration column drop down menu const bool bInShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder( bInShouldCloseWindowAfterMenuSelection, NULL ); { FUIAction Action( FExecuteAction::CreateSP( this, &SEventsTree::SetDurationUnits, EVisualizerTimeUnits::Microseconds ), FCanExecuteAction(), FIsActionChecked::CreateSP( this, &SEventsTree::CheckDurationUnits, EVisualizerTimeUnits::Microseconds ) ); MenuBuilder.AddMenuEntry( LOCTEXT("Microseconds", "Microseconds"), FText(), FSlateIcon(), Action, NAME_None, EUserInterfaceActionType::Check ); } { FUIAction Action( FExecuteAction::CreateSP( this, &SEventsTree::SetDurationUnits, EVisualizerTimeUnits::Milliseconds ), FCanExecuteAction(), FIsActionChecked::CreateSP( this, &SEventsTree::CheckDurationUnits, EVisualizerTimeUnits::Milliseconds ) ); MenuBuilder.AddMenuEntry( LOCTEXT("Milliseconds", "Milliseconds"), FText(), FSlateIcon(), Action, NAME_None, EUserInterfaceActionType::Check ); } { FUIAction Action( FExecuteAction::CreateSP( this, &SEventsTree::SetDurationUnits, EVisualizerTimeUnits::Seconds ), FCanExecuteAction(), FIsActionChecked::CreateSP( this, &SEventsTree::CheckDurationUnits, EVisualizerTimeUnits::Seconds ) ); MenuBuilder.AddMenuEntry( LOCTEXT("Seconds", "Seconds"), FText(), FSlateIcon(), Action, NAME_None, EUserInterfaceActionType::Check ); } // Name column drop down menu FMenuBuilder NameMenuBuilder( bInShouldCloseWindowAfterMenuSelection, NULL ); { FUIAction Action( FExecuteAction::CreateSP( this, &SEventsTree::SetViewMode, EVisualizerViewMode::Hierarchical ), FCanExecuteAction(), FIsActionChecked::CreateSP( this, &SEventsTree::CheckViewMode, EVisualizerViewMode::Hierarchical ) ); NameMenuBuilder.AddMenuEntry( LOCTEXT("Hierarchical", "Hierarchical"), FText(), FSlateIcon(), Action, NAME_None, EUserInterfaceActionType::Check ); } { FUIAction Action( FExecuteAction::CreateSP( this, &SEventsTree::SetViewMode, EVisualizerViewMode::Flat ), FCanExecuteAction(), FIsActionChecked::CreateSP( this, &SEventsTree::CheckViewMode, EVisualizerViewMode::Flat ) ); NameMenuBuilder.AddMenuEntry( LOCTEXT("Flat", "Flat"), FText(), FSlateIcon(), Action, NAME_None, EUserInterfaceActionType::Check ); } { FUIAction Action( FExecuteAction::CreateSP( this, &SEventsTree::SetViewMode, EVisualizerViewMode::Coalesced ), FCanExecuteAction(), FIsActionChecked::CreateSP( this, &SEventsTree::CheckViewMode, EVisualizerViewMode::Coalesced ) ); NameMenuBuilder.AddMenuEntry( LOCTEXT("Coalesced", "Coalesced"), FText(), FSlateIcon(), Action, NAME_None, EUserInterfaceActionType::Check ); } { FUIAction Action( FExecuteAction::CreateSP( this, &SEventsTree::SetViewMode, EVisualizerViewMode::FlatCoalesced ), FCanExecuteAction(), FIsActionChecked::CreateSP( this, &SEventsTree::CheckViewMode, EVisualizerViewMode::FlatCoalesced ) ); NameMenuBuilder.AddMenuEntry( LOCTEXT("FlatCoalesced", "Flat Coalesced"), FText(), FSlateIcon(), Action, NAME_None, EUserInterfaceActionType::Check ); } this->ChildSlot [ SNew( SVerticalBox ) +SVerticalBox::Slot().AutoHeight().Padding( 1.0f, 0.0f, 1.0f, 2.0f ) [ SNew( SSearchBox ) .ToolTipText( NSLOCTEXT("ProfileVisualizer", "FilterSearchHint", "Type here to search events.") ) .OnTextChanged( this, &SEventsTree::OnFilterTextChanged ) .OnTextCommitted( this, &SEventsTree::OnFilterTextCommitted ) ] +SVerticalBox::Slot().Padding( 2 ).FillHeight( 1 ).VAlign( VAlign_Fill ) [ SNew( SHorizontalBox ) +SHorizontalBox::Slot().Padding( 2 ).FillWidth( 1 ).HAlign( HAlign_Fill ) [ // List of all events for the selected thread SAssignNew( EventsListView, STreeView< TSharedPtr< FVisualizerEvent > > ) // Tell the list view where to get its source data .TreeItemsSource( &SelectedEventsView ) // When the list view needs to generate a widget for some data item, use this method .OnGenerateRow( this, &SEventsTree::OnGenerateWidgetForEventsList ) // Given some DataItem, this is how we find out if it has any children and what they are. .OnGetChildren( this, &SEventsTree::OnGetChildrenForEventsList ) // Selection mode .SelectionMode( ESelectionMode::Single ) // Selection callback .OnSelectionChanged( this, &SEventsTree::OnEventSelectionChanged ) .HeaderRow ( SNew( SHeaderRow ) + SHeaderRow::Column( NAME_NameColumn ) .DefaultLabel( NSLOCTEXT("ProfileVisualizer", "ColumnName", "Name") ) .SortMode( TAttribute< EColumnSortMode::Type >::Create( TAttribute< EColumnSortMode::Type >::FGetter::CreateSP( this, &SEventsTree::GetColumnSortMode, NAME_NameColumn ) ) ) .OnSort( FOnSortModeChanged::CreateSP( this, &SEventsTree::OnColumnSortModeChanged ) ) .FillWidth( 1.0f ) .MenuContent() [ NameMenuBuilder.MakeWidget() ] + SHeaderRow::Column( NAME_DurationColumn ) .DefaultLabel( this, &SEventsTree::GetDurationColumnTitle ) .SortMode( TAttribute< EColumnSortMode::Type >::Create( TAttribute< EColumnSortMode::Type >::FGetter::CreateSP( this, &SEventsTree::GetColumnSortMode, NAME_DurationColumn ) ) ) .OnSort( FOnSortModeChanged::CreateSP( this, &SEventsTree::OnColumnSortModeChanged ) ) .FixedWidth( 128.0f ) .MenuContent() [ MenuBuilder.MakeWidget() ] ) ] ] ]; } END_SLATE_FUNCTION_BUILD_OPTIMIZATION EColumnSortMode::Type SEventsTree::GetColumnSortMode( const FName ColumnId ) { if ( SortByColumn != ColumnId ) { return EColumnSortMode::None; } return SortMode; } FText SEventsTree::GetDurationColumnTitle() const { static const FText Units[] = { NSLOCTEXT("ProfileVisualizer", "microseconds", "microseconds"), NSLOCTEXT("ProfileVisualizer", "milliseconds", "ms"), NSLOCTEXT("ProfileVisualizer", "seconds", "s") }; return FText::Format( NSLOCTEXT("ProfileVisualizer", "ColumnDurationValue", "Duration ({0})"), Units[ DurationUnits ] ); } void ClearEventsSelection( TArray< TSharedPtr< FVisualizerEvent > >& Events ) { for( int32 Index = 0; Index < Events.Num(); ++Index ) { TSharedPtr< FVisualizerEvent > CurrentEvent = Events[ Index ]; CurrentEvent->IsSelected = false; // Clear recursively if( CurrentEvent->Children.Num() ) { ClearEventsSelection( CurrentEvent->Children ); } } } void SEventsTree::OnEventSelectionChanged( TSharedPtr< FVisualizerEvent > Selection, ESelectInfo::Type /*SelectInfo*/ ) { if( Selection.IsValid() ) { ClearEventsSelection( SelectedEventsView ); Selection->IsSelected = true; // Mirror the selection in the source events list TSharedPtr< FVisualizerEvent > MappedSelection = ViewToEventsMap.FindChecked( Selection ); MappedSelection->IsSelected = true; if( !bSuppressSelectionChangedEvent ) { OnEventSelectionChangedDelegate.ExecuteIfBound( MappedSelection ); } } else if( !bSuppressSelectionChangedEvent ) { OnEventSelectionChangedDelegate.ExecuteIfBound( Selection ); } } TSharedRef< ITableRow > SEventsTree::OnGenerateWidgetForEventsList( TSharedPtr< FVisualizerEvent > InItem, const TSharedRef& OwnerTable ) { return SNew( SEventItem, OwnerTable ) .EventName( InItem->EventName ) .EventDuration( this, &SEventsTree::GetEventDuration, InItem->DurationMs ); } double SEventsTree::GetEventDuration( double InDurationMs ) const { switch( DurationUnits ) { case EVisualizerTimeUnits::Microseconds: return InDurationMs * 1000.0; case EVisualizerTimeUnits::Milliseconds: return InDurationMs; case EVisualizerTimeUnits::Seconds: return InDurationMs * 0.001; } return InDurationMs; } void SEventsTree::SetDurationUnits( EVisualizerTimeUnits::Type InUnits ) { DurationUnits = InUnits; EventsListView->RequestTreeRefresh(); } void SEventsTree::SetViewMode( EVisualizerViewMode::Type InMode ) { ViewMode = InMode; CreateSelectedEventsView(); SortEventsList(); EventsListView->RequestTreeRefresh(); } void SEventsTree::OnGetChildrenForEventsList( TSharedPtr InItem, TArray >& OutChildren ) { OutChildren = InItem->Children; } void SEventsTree::OnColumnSortModeChanged( const EColumnSortPriority::Type SortPriority, const FName& ColumnId, const EColumnSortMode::Type InSortMode ) { SortByColumn = ColumnId; SortMode = InSortMode; SortEventsList(); } void SEventsTree::SortEventsList( TArray< TSharedPtr< FVisualizerEvent > >& Events ) { // Sort taking the current settings into account if( SortByColumn == NAME_NameColumn ) { if( SortMode == EColumnSortMode::Ascending ) { struct FCompareEventsByName { FORCEINLINE bool operator()( const TSharedPtr A, const TSharedPtr B ) const { return A->EventName < B->EventName; } }; Events.Sort( FCompareEventsByName() ); } else if( SortMode == EColumnSortMode::Descending ) { struct FCompareEventsByNameDescending { FORCEINLINE bool operator()( const TSharedPtr A, const TSharedPtr B ) const { return B->EventName < A->EventName; } }; Events.Sort( FCompareEventsByNameDescending() ); } } else if( SortByColumn == NAME_DurationColumn ) { if( SortMode == EColumnSortMode::Ascending ) { struct FCompareEventsByDuration { FORCEINLINE bool operator()( const TSharedPtr A, const TSharedPtr B ) const { return A->DurationMs < B->DurationMs; } }; Events.Sort( FCompareEventsByDuration() ); } else if( SortMode == EColumnSortMode::Descending ) { struct FCompareEventsByDurationDescending { FORCEINLINE bool operator()( const TSharedPtr A, const TSharedPtr B ) const { return B->DurationMs < A->DurationMs; } }; Events.Sort( FCompareEventsByDurationDescending() ); } } // Sort recursively for( int32 Index = 0; Index < Events.Num(); Index++ ) { if( Events[ Index ]->Children.Num() ) { SortEventsList( Events[ Index ]->Children ); } } } void SEventsTree::SortEventsList( void ) { SortEventsList( SelectedEventsView ); EventsListView->RequestTreeRefresh(); RestoreEventSelection( SelectedEventsView ); } bool SEventsTree::RestoreEventSelection( TArray< TSharedPtr< FVisualizerEvent > >& Events ) { // Search for the selected event for( int32 Index = 0; Index < Events.Num(); ++Index ) { if( Events[ Index ]->IsSelected ) { // Select it in the tree view widget EventsListView->ClearSelection(); EventsListView->RequestTreeRefresh(); EventsListView->SetSelection( Events[ Index ] ); EventsListView->RequestScrollIntoView( Events[ Index ] ); return true; } // Search recursively if( RestoreEventSelection( Events[ Index ]->Children ) ) { return true; } } return false; } void SEventsTree::HandleBarGraphSelectionChanged( TSharedPtr< FVisualizerEvent > Selection ) { if ( Selection.IsValid() ) { for( int32 EventIndex = 0; EventIndex < SelectedEvents.Num(); EventIndex++ ) { TSharedPtr Event = SelectedEvents[ EventIndex ]; if( Selection->EventName == Event->EventName ) { HandleBarEventSelectionChanged( SelectedThread->Category, Event ); break; } } } } void SEventsTree::HandleBarGraphExpansionChanged( TSharedPtr< FVisualizerEvent > Selection ) { if ( Selection.IsValid() ) { // We don't want to trigger selection changed event when the selection change is actually coming from the bar graph bSuppressSelectionChangedEvent = true; SelectedThread = Selection; SelectedEvents = Selection.Get()->Children; CreateSelectedEventsView(); SortEventsList(); bSuppressSelectionChangedEvent = false; } } void SEventsTree::HandleBarEventSelectionChanged( int32 Thread, TSharedPtr Selection ) { const TSharedPtr< FVisualizerEvent >* MappedViewSelection = ViewToEventsMap.FindKey( Selection ); if( MappedViewSelection ) { TSharedPtr< FVisualizerEvent > ViewSelection = *MappedViewSelection; // Clear the current selection EventsListView->ClearSelection(); EventsListView->RequestTreeRefresh(); // Expand all parents so that it's visible for( TSharedPtr< FVisualizerEvent > ParentEvent = ViewSelection->ParentEvent; ParentEvent.IsValid(); ParentEvent = ParentEvent->ParentEvent ) { EventsListView->SetItemExpansion( ParentEvent, true ); } EventsListView->SetSelection( ViewSelection ); EventsListView->RequestScrollIntoView( ViewSelection ); } else { EventsListView->ClearSelection(); } } FString SEventsTree::GetTabTitle() const { if ( SelectedThread.IsValid() ) { return SelectedThread->EventName; } else { return NSLOCTEXT("ProfileVisualizer", "EventsVisualizerName", "Empty Events List").ToString(); } } int32 SEventsTree::CountEvents( TArray< TSharedPtr< FVisualizerEvent > >& Events ) { int32 Count = Events.Num(); for( int32 Index = 0; Index < Events.Num(); Index++ ) { Count += CountEvents( Events[ Index ]->Children ); } return Count; } void SEventsTree::CreateSelectedEventsView() { const int32 EventsCount = CountEvents( SelectedEvents ); ViewToEventsMap.Empty( EventsCount ); SelectedEventsView.Empty( SelectedEvents.Num() ); // Create the selected events copy based on the current view mode if( ViewMode == EVisualizerViewMode::Hierarchical ) { SelectedEventsView.Empty( SelectedEvents.Num() ); for( int32 Index = 0; Index < SelectedEvents.Num(); Index++ ) { if( FilterEvent( SelectedEvents[ Index ] ) ) { TSharedPtr< FVisualizerEvent > EventCopy( CreateSelectedEventsViewRecursively( SelectedEvents[ Index ] ) ); if( EventCopy.IsValid() ) { SelectedEventsView.Add( EventCopy ); } } } } else if( ViewMode == EVisualizerViewMode::Flat ) { SelectedEventsView.Empty( EventsCount ); for( int32 Index = 0; Index < SelectedEvents.Num(); Index++ ) { CreateSelectedEventsViewRecursivelyAndFlatten( SelectedEvents[ Index ] ); } } else if( ViewMode == EVisualizerViewMode::Coalesced ) { SelectedEventsView.Empty( SelectedEvents.Num() ); CreateSelectedEventsViewRecursivelyCoalesced( SelectedEvents, SelectedEventsView, TSharedPtr< FVisualizerEvent >() ); } else if( ViewMode == EVisualizerViewMode::FlatCoalesced ) { SelectedEventsView.Empty( EventsCount ); CreateSelectedEventsViewRecursivelyFlatCoalesced( SelectedEvents ); } } TSharedPtr< FVisualizerEvent > SEventsTree::CreateSelectedEventsViewRecursively( TSharedPtr< FVisualizerEvent > SourceEvent ) { TSharedPtr< FVisualizerEvent > EventCopy( new FVisualizerEvent( *SourceEvent ) ); EventCopy->Children.Empty( SourceEvent->Children.Num() ); for( int32 ChildIndex = 0; ChildIndex < SourceEvent->Children.Num(); ChildIndex++ ) { if( FilterEvent( SourceEvent->Children[ ChildIndex ] ) ) { TSharedPtr< FVisualizerEvent > ChildCopy = CreateSelectedEventsViewRecursively( SourceEvent->Children[ ChildIndex ] ); if( ChildCopy.IsValid() ) { ChildCopy->ParentEvent = EventCopy; EventCopy->Children.Add( ChildCopy ); } } } // Add this event because it's a leaf or has valid children if( EventCopy->Children.Num() > 0 || SourceEvent->Children.Num() == 0 ) { ViewToEventsMap.Add( EventCopy, SourceEvent ); } else { EventCopy.Reset(); } return EventCopy; } void SEventsTree::CreateSelectedEventsViewRecursivelyAndFlatten( TSharedPtr< FVisualizerEvent > SourceEvent ) { // Collect only children and store them directly into SelectedEventsView if ( FilterEvent( SourceEvent ) && SourceEvent->Children.Num() == 0 ) { TSharedPtr< FVisualizerEvent > EventCopy( new FVisualizerEvent( *SourceEvent ) ); EventCopy->ParentEvent.Reset(); ViewToEventsMap.Add( EventCopy, SourceEvent ); SelectedEventsView.Add( EventCopy ); } for( int32 ChildIndex = 0; ChildIndex < SourceEvent->Children.Num(); ChildIndex++ ) { CreateSelectedEventsViewRecursivelyAndFlatten( SourceEvent->Children[ ChildIndex ] ); } } void SEventsTree::CreateSelectedEventsViewRecursivelyCoalesced( TArray< TSharedPtr< FVisualizerEvent > >& SourceEvents, TArray< TSharedPtr< FVisualizerEvent > >& CopiedEvents, TSharedPtr< FVisualizerEvent > InParent ) { for( int32 SourceIndex = 0; SourceIndex < SourceEvents.Num(); SourceIndex++ ) { TSharedPtr< FVisualizerEvent > SourceEvent = SourceEvents[ SourceIndex ]; if( FilterEvent( SourceEvent ) ) { if( SourceEvent->Children.Num() == 0 ) { // Check if a child with the same name has already been added to the copied event bool bEventExists = false; for( int32 CopiedEventIndex = 0; CopiedEventIndex < CopiedEvents.Num(); CopiedEventIndex++ ) { if( CopiedEvents[ CopiedEventIndex ]->EventName == SourceEvent->EventName ) { bEventExists = true; break; } } if( !bEventExists ) { TSharedPtr< FVisualizerEvent > EventCopy( new FVisualizerEvent( *SourceEvent ) ); EventCopy->ParentEvent = InParent; ViewToEventsMap.Add( EventCopy, SourceEvent ); CopiedEvents.Add( EventCopy ); // Find other children with the same name and add their time to the copied one for( int32 OtherIndex = SourceIndex + 1; OtherIndex < SourceEvents.Num(); OtherIndex++ ) { TSharedPtr< FVisualizerEvent > OtherEvent = SourceEvents[ OtherIndex ]; if( OtherEvent->Children.Num() == 0 && OtherEvent->EventName == SourceEvent->EventName ) { EventCopy->DurationMs += OtherEvent->DurationMs; } } } } else { TSharedPtr< FVisualizerEvent > EventCopy( new FVisualizerEvent( *SourceEvent ) ); EventCopy->Children.Empty( SourceEvent->Children.Num() ); EventCopy->ParentEvent = InParent; CreateSelectedEventsViewRecursivelyCoalesced( SourceEvent->Children, EventCopy->Children, EventCopy ); // Only add this event if its children haven't been filtered if( EventCopy->Children.Num() ) { ViewToEventsMap.Add( EventCopy, SourceEvent ); CopiedEvents.Add( EventCopy ); } } } } } void SEventsTree::CreateSelectedEventsViewRecursivelyFlatCoalesced( TArray< TSharedPtr< FVisualizerEvent > >& SourceEvents ) { for( int32 SourceIndex = 0; SourceIndex < SourceEvents.Num(); SourceIndex++ ) { TSharedPtr< FVisualizerEvent > SourceEvent = SourceEvents[ SourceIndex ]; if( FilterEvent( SourceEvent ) ) { if( SourceEvent->Children.Num() == 0 ) { // Check if a child with the same name has already been added to the copied event bool bEventExists = false; for( int32 CopiedEventIndex = 0; CopiedEventIndex < SelectedEventsView.Num(); CopiedEventIndex++ ) { if( SelectedEventsView[ CopiedEventIndex ]->EventName == SourceEvent->EventName ) { bEventExists = true; break; } } if( !bEventExists ) { TSharedPtr< FVisualizerEvent > EventCopy( new FVisualizerEvent( *SourceEvent ) ); EventCopy->ParentEvent.Reset(); ViewToEventsMap.Add( EventCopy, SourceEvent ); SelectedEventsView.Add( EventCopy ); // Find other children with the same name and add their time to the copied one for( int32 OtherIndex = SourceIndex + 1; OtherIndex < SourceEvents.Num(); OtherIndex++ ) { TSharedPtr< FVisualizerEvent > OtherEvent = SourceEvents[ OtherIndex ]; if( OtherEvent->Children.Num() == 0 && OtherEvent->EventName == SourceEvent->EventName ) { EventCopy->DurationMs += OtherEvent->DurationMs; } } } } else { CreateSelectedEventsViewRecursivelyFlatCoalesced( SourceEvent->Children ); } } } } void SEventsTree::OnFilterTextChanged( const FText& InFilterText ) { FilterText = InFilterText.ToString(); CreateSelectedEventsView(); EventsListView->RequestTreeRefresh(); } void SEventsTree::OnFilterTextCommitted( const FText& InFilterText, ETextCommit::Type /*CommitInfo*/ ) { } #undef LOCTEXT_NAMESPACE