// Copyright Epic Games, Inc. All Rights Reserved. #include "SStatsView.h" #include "DesktopPlatformModule.h" #include "Framework/Commands/Commands.h" #include "Framework/Commands/UICommandList.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "GenericPlatform/GenericPlatformTime.h" #include "HAL/PlatformApplicationMisc.h" #include "HAL/PlatformFileManager.h" #include "Logging/MessageLog.h" #include "SlateOptMacros.h" #include "Styling/AppStyle.h" #include "Styling/StyleColors.h" #include "Widgets/Colors/SColorPicker.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SSearchBox.h" #include "Widgets/Layout/SGridPanel.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/SToolTip.h" #include "Widgets/Views/STableViewBase.h" // TraceServices #include "TraceServices/Model/Counters.h" // TraceInsightsCore #include "InsightsCore/Common/Stopwatch.h" #include "InsightsCore/Common/TimeUtils.h" #include "InsightsCore/Table/ViewModels/Table.h" #include "InsightsCore/Table/ViewModels/TableCellValueSorter.h" #include "InsightsCore/Table/ViewModels/TableColumn.h" #include "InsightsCore/Table/Widgets/SAsyncOperationStatus.h" // TraceInsights #include "Insights/InsightsStyle.h" #include "Insights/Log.h" #include "Insights/TimingProfiler/TimingProfilerManager.h" #include "Insights/TimingProfiler/ViewModels/CounterAggregator.h" #include "Insights/TimingProfiler/ViewModels/StatsNodeHelper.h" #include "Insights/TimingProfiler/ViewModels/StatsViewColumnFactory.h" #include "Insights/TimingProfiler/ViewModels/TimingExporter.h" #include "Insights/TimingProfiler/Widgets/SStatsTableRow.h" #include "Insights/TimingProfiler/Widgets/SStatsViewTooltip.h" #include "Insights/TimingProfiler/Widgets/STimingProfilerWindow.h" #include "Insights/TimingProfilerCommon.h" #include "Insights/ViewModels/TimingGraphTrack.h" #include "Insights/Widgets/STimingView.h" #include "Insights/Table/ViewModels/TableCommands.h" #include #define LOCTEXT_NAMESPACE "UE::Insights::TimingProfiler::SStatsView" namespace UE::Insights::TimingProfiler { //////////////////////////////////////////////////////////////////////////////////////////////////// // FStatsViewCommands //////////////////////////////////////////////////////////////////////////////////////////////////// class FStatsViewCommands : public TCommands { public: FStatsViewCommands() : TCommands( TEXT("CountersViewCommands"), NSLOCTEXT("Contexts", "CountersViewCommands", "Insights - Counters View"), NAME_None, FInsightsStyle::GetStyleSetName()) { } virtual ~FStatsViewCommands() { } // UI_COMMAND takes long for the compiler to optimize UE_DISABLE_OPTIMIZATION_SHIP virtual void RegisterCommands() override { UI_COMMAND(Command_GenerateNewColor, "Generate New Color", "Generates a new random color for the selected counters.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(Command_EditColor, "Edit Color...", "Changes color for the selected counters.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(Command_CopyToClipboard, "Copy To Clipboard", "Copies the selection (counters and their aggregated statistics) to clipboard.", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control, EKeys::C)); UI_COMMAND(Command_CopyNameToClipboard, "Copy Name To Clipboard", "Copies the name of the selected counter to the clipboard.", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control | EModifierKey::Shift, EKeys::C)); UI_COMMAND(Command_Export, "Export...", "Exports the selection (counters and their aggregated statistics) to a text file (tab-separated values or comma-separated values).", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control, EKeys::S)); UI_COMMAND(Command_ExportValues, "Export Values...", "Exports the values of the selected counter to a text file (tab-separated values or comma-separated values).\nExports the values only in the selected time region (if any) or the entire session if no time region is selected.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(Command_ExportOps, "Export Operations...", "Exports the incremental operations/values of the selected counter to a text file (tab-separated values or comma-separated values).\nExports the ops/values only in the selected time region (if any) or the entire session if no time region is selected.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(Command_ExportCounters, "Export Counters...", "Exports the list of counters to a text file (tab-separated values or comma-separated values).", EUserInterfaceActionType::Button, FInputChord()); } UE_ENABLE_OPTIMIZATION_SHIP TSharedPtr Command_GenerateNewColor; TSharedPtr Command_EditColor; TSharedPtr Command_CopyToClipboard; TSharedPtr Command_CopyNameToClipboard; TSharedPtr Command_Export; TSharedPtr Command_ExportValues; TSharedPtr Command_ExportOps; TSharedPtr Command_ExportCounters; }; //////////////////////////////////////////////////////////////////////////////////////////////////// // SStatsView //////////////////////////////////////////////////////////////////////////////////////////////////// SStatsView::SStatsView() : Table(MakeShared()) , bExpansionSaved(false) , bFilterOutZeroCountStats(false) , GroupingMode(EStatsGroupingMode::Flat) , AvailableSorters() , CurrentSorter(nullptr) , ColumnBeingSorted(GetDefaultColumnBeingSorted()) , ColumnSortMode(GetDefaultColumnSortMode()) , Aggregator(MakeShared()) { FMemory::Memset(FilterByNodeType, 1); FMemory::Memset(FilterByDataType, 1); } //////////////////////////////////////////////////////////////////////////////////////////////////// SStatsView::~SStatsView() { // Remove ourselves from the Insights manager. if (FInsightsManager::Get().IsValid()) { FInsightsManager::Get()->GetSessionChangedEvent().RemoveAll(this); FInsightsManager::Get()->GetSessionAnalysisCompletedEvent().RemoveAll(this); } FStatsViewCommands::Unregister(); Session.Reset(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::InitCommandList() { FStatsViewCommands::Register(); CommandList = MakeShared(); CommandList->MapAction(FStatsViewCommands::Get().Command_GenerateNewColor, FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_GenerateNewColor_Execute), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_GenerateNewColor_CanExecute)); CommandList->MapAction(FStatsViewCommands::Get().Command_EditColor, FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_EditColor_Execute), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_EditColor_CanExecute)); CommandList->MapAction(FStatsViewCommands::Get().Command_CopyToClipboard, FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_CopySelectedToClipboard_Execute), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_CopySelectedToClipboard_CanExecute)); CommandList->MapAction(FStatsViewCommands::Get().Command_CopyNameToClipboard, FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_CopySelectedNameToClipboard_Execute), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_CopySelectedNameToClipboard_CanExecute)); CommandList->MapAction(FStatsViewCommands::Get().Command_Export, FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_Export_Execute), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_Export_CanExecute)); CommandList->MapAction(FStatsViewCommands::Get().Command_ExportValues, FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ExportValues_Execute), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ExportValues_CanExecute)); CommandList->MapAction(FStatsViewCommands::Get().Command_ExportOps, FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ExportOps_Execute), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ExportOps_CanExecute)); CommandList->MapAction(FStatsViewCommands::Get().Command_ExportCounters, FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ExportCounters_Execute), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ExportCounters_CanExecute)); } //////////////////////////////////////////////////////////////////////////////////////////////////// BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SStatsView::Construct(const FArguments& InArgs) { SAssignNew(ExternalScrollbar, SScrollBar) .AlwaysShowScrollbar(true); ChildSlot [ SNew(SVerticalBox) + SVerticalBox::Slot() .VAlign(VAlign_Center) .AutoHeight() .Padding(2.0f, 2.0f, 2.0f, 2.0f) [ SNew(SVerticalBox) + SVerticalBox::Slot() .VAlign(VAlign_Center) .Padding(2.0f) .AutoHeight() [ SNew(SHorizontalBox) // Search box + SHorizontalBox::Slot() .Padding(2.0f) .FillWidth(1.0f) .VAlign(VAlign_Center) [ SAssignNew(SearchBox, SSearchBox) .HintText(LOCTEXT("SearchBoxHint", "Search counters or groups")) .OnTextChanged(this, &SStatsView::SearchBox_OnTextChanged) .IsEnabled(this, &SStatsView::SearchBox_IsEnabled) .ToolTipText(LOCTEXT("FilterSearchHint", "Type here to search counter or group.")) ] // Filter by type (Int64) + SHorizontalBox::Slot() .Padding(4.0f, 0.0f, 4.0f, 0.0f) .AutoWidth() .VAlign(VAlign_Center) [ GetToggleButtonForDataType(EStatsNodeDataType::Int64) ] // Filter by type (Double) + SHorizontalBox::Slot() .Padding(4.0f, 0.0f, 4.0f, 0.0f) .AutoWidth() .VAlign(VAlign_Center) [ GetToggleButtonForDataType(EStatsNodeDataType::Double) ] // Filter out counters with zero instance count + SHorizontalBox::Slot() .Padding(2.0f) .AutoWidth() .VAlign(VAlign_Center) [ SNew(SCheckBox) .Style(FAppStyle::Get(), "ToggleButtonCheckbox") .HAlign(HAlign_Center) .Padding(3.0f) .OnCheckStateChanged(this, &SStatsView::FilterOutZeroCountStats_OnCheckStateChanged) .IsChecked(this, &SStatsView::FilterOutZeroCountStats_IsChecked) .ToolTipText(LOCTEXT("FilterOutZeroCountStats_Tooltip", "Filter out the counters having zero total instance count (aggregated stats).")) [ SNew(SImage) .Image(FInsightsStyle::Get().GetBrush("Icons.ZeroCountFilter")) ] ] ] // Group by + SVerticalBox::Slot() .VAlign(VAlign_Center) .Padding(2.0f) .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(0.0f, 0.0f, 4.0f, 0.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("GroupByText", "Group by")) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SBox) .MinDesiredWidth(128.0f) [ SAssignNew(GroupByComboBox, SComboBox>) .ToolTipText(this, &SStatsView::GroupBy_GetSelectedTooltipText) .OptionsSource(&GroupByOptionsSource) .OnSelectionChanged(this, &SStatsView::GroupBy_OnSelectionChanged) .OnGenerateWidget(this, &SStatsView::GroupBy_OnGenerateWidget) [ SNew(STextBlock) .Text(this, &SStatsView::GroupBy_GetSelectedText) ] ] ] ] ] // Tree view + SVerticalBox::Slot() .FillHeight(1.0f) .Padding(0.0f, 2.0f, 0.0f, 0.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(0.0f) [ SNew(SOverlay) + SOverlay::Slot() .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) [ SAssignNew(TreeView, STreeView) .ExternalScrollbar(ExternalScrollbar) .SelectionMode(ESelectionMode::Multi) .TreeItemsSource(&FilteredGroupNodes) .OnGetChildren(this, &SStatsView::TreeView_OnGetChildren) .OnGenerateRow(this, &SStatsView::TreeView_OnGenerateRow) .OnSelectionChanged(this, &SStatsView::TreeView_OnSelectionChanged) .OnMouseButtonDoubleClick(this, &SStatsView::TreeView_OnMouseButtonDoubleClick) .OnContextMenuOpening(FOnContextMenuOpening::CreateSP(this, &SStatsView::TreeView_GetMenuContent)) .HeaderRow ( SAssignNew(TreeViewHeaderRow, SHeaderRow) .Visibility(EVisibility::Visible) ) ] + SOverlay::Slot() .HAlign(HAlign_Right) .VAlign(VAlign_Bottom) .Padding(16.0f) [ SAssignNew(AsyncOperationStatus, SAsyncOperationStatus, Aggregator) ] ] + SHorizontalBox::Slot() .AutoWidth() .Padding(0.0f) [ SNew(SBox) .WidthOverride(FOptionalSize(13.0f)) [ ExternalScrollbar.ToSharedRef() ] ] ] // Status bar + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Fill) .Padding(0.0f) [ SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("WhiteBrush")) .BorderBackgroundColor(FLinearColor(0.05f, 0.1f, 0.2f, 1.0f)) .HAlign(HAlign_Center) [ SNew(STextBlock) .Margin(FMargin(4.0f, 1.0f, 4.0f, 1.0f)) .Text(LOCTEXT("EmptyAggregationNote", "Please select a time range to update the aggregated statistics!")) .ColorAndOpacity(FLinearColor(1.0f, 0.75f, 0.5f, 1.0f)) .Visibility_Lambda([this]() { return Aggregator->IsEmptyTimeInterval() && !Aggregator->IsRunning() ? EVisibility::Visible : EVisibility::Collapsed; }) ] ] ]; InitializeAndShowHeaderColumns(); // Create the search filters: text based, type based etc. TextFilter = MakeShared(FStatsNodeTextFilter::FItemToStringArray::CreateSP(this, &SStatsView::HandleItemToStringArray)); Filters = MakeShared(); Filters->Add(TextFilter); CreateGroupByOptionsSources(); CreateSortings(); InitCommandList(); // Register ourselves with the Insights manager. FInsightsManager::Get()->GetSessionChangedEvent().AddSP(this, &SStatsView::InsightsManager_OnSessionChanged); FInsightsManager::Get()->GetSessionAnalysisCompletedEvent().AddSP(this, &SStatsView::InsightsManager_OnSessionAnalysisCompleted); // Update the Session (i.e. when analysis session was already started). InsightsManager_OnSessionChanged(); } END_SLATE_FUNCTION_BUILD_OPTIMIZATION //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedPtr SStatsView::TreeView_GetMenuContent() { const TArray SelectedNodes = TreeView->GetSelectedItems(); const int32 NumSelectedNodes = SelectedNodes.Num(); FStatsNodePtr SelectedNode = NumSelectedNodes ? SelectedNodes[0] : nullptr; FText SelectionStr; if (NumSelectedNodes == 0) { SelectionStr = LOCTEXT("NothingSelected", "Nothing selected"); } else if (NumSelectedNodes == 1) { FString ItemName = SelectedNode->GetName().ToString(); const int32 MaxStringLen = 64; if (ItemName.Len() > MaxStringLen) { ItemName = ItemName.Left(MaxStringLen) + TEXT("..."); } SelectionStr = FText::FromString(ItemName); } else { SelectionStr = FText::Format(LOCTEXT("MultipleSelection_Fmt", "{0} selected items"), FText::AsNumber(NumSelectedNodes)); } const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, CommandList.ToSharedRef()); // Selection menu MenuBuilder.BeginSection("Selection", LOCTEXT("ContextMenu_Section_Selection", "Selection")); { struct FLocal { static bool ReturnFalse() { return false; } }; FUIAction DummyUIAction; DummyUIAction.CanExecuteAction = FCanExecuteAction::CreateStatic(&FLocal::ReturnFalse); MenuBuilder.AddMenuEntry ( SelectionStr, LOCTEXT("ContextMenu_Selection", "Currently selected items"), FSlateIcon(), DummyUIAction, NAME_None, EUserInterfaceActionType::Button ); } MenuBuilder.EndSection(); // Counter options section MenuBuilder.BeginSection("CounterOptions", LOCTEXT("ContextMenu_Section_CounterOptions", "Counter Options")); { auto CanExecute = [NumSelectedNodes, SelectedNode]() { TSharedPtr Wnd = FTimingProfilerManager::Get()->GetProfilerWindow(); TSharedPtr TimingView = Wnd.IsValid() ? Wnd->GetTimingView() : nullptr; return TimingView.IsValid() && NumSelectedNodes == 1 && SelectedNode.IsValid() && SelectedNode->GetType() != EStatsNodeType::Group; }; // Add/remove series to/from graph track { FUIAction Action_ToggleCounterInGraphTrack; Action_ToggleCounterInGraphTrack.CanExecuteAction = FCanExecuteAction::CreateLambda(CanExecute); Action_ToggleCounterInGraphTrack.ExecuteAction = FExecuteAction::CreateSP(this, &SStatsView::ToggleTimingViewMainGraphEventSeries, SelectedNode); if (SelectedNode.IsValid() && SelectedNode->GetType() != EStatsNodeType::Group && IsSeriesInTimingViewMainGraph(SelectedNode)) { MenuBuilder.AddMenuEntry ( LOCTEXT("ContextMenu_RemoveFromGraphTrack", "Remove series from graph track"), LOCTEXT("ContextMenu_RemoveFromGraphTrack_Desc", "Removes the series containing event instances of the selected counter from the Main Graph track."), FSlateIcon(FInsightsStyle::GetStyleSetName(), "Icons.RemoveGraphSeries"), Action_ToggleCounterInGraphTrack, NAME_None, EUserInterfaceActionType::Button ); } else { MenuBuilder.AddMenuEntry ( LOCTEXT("ContextMenu_AddToGraphTrack", "Add series to graph track"), LOCTEXT("ContextMenu_AddToGraphTrack_Desc", "Adds a series containing event instances of the selected counter to the Main Graph track."), FSlateIcon(FInsightsStyle::GetStyleSetName(), "Icons.AddGraphSeries"), Action_ToggleCounterInGraphTrack, NAME_None, EUserInterfaceActionType::Button ); } } MenuBuilder.AddMenuEntry ( FStatsViewCommands::Get().Command_GenerateNewColor, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Refresh") ); MenuBuilder.AddMenuEntry ( FStatsViewCommands::Get().Command_EditColor, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.EyeDropper") ); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("Misc", LOCTEXT("ContextMenu_Section_Misc", "Miscellaneous")); { MenuBuilder.AddMenuEntry ( FStatsViewCommands::Get().Command_CopyToClipboard, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "GenericCommands.Copy") ); MenuBuilder.AddMenuEntry ( FStatsViewCommands::Get().Command_CopyNameToClipboard, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "GenericCommands.Copy") ); MenuBuilder.AddMenuEntry ( FStatsViewCommands::Get().Command_Export, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Save") ); MenuBuilder.AddMenuEntry ( FStatsViewCommands::Get().Command_ExportValues, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Save") ); MenuBuilder.AddMenuEntry ( FStatsViewCommands::Get().Command_ExportOps, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Save") ); MenuBuilder.AddMenuEntry ( FStatsViewCommands::Get().Command_ExportCounters, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.Save") ); } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::TreeView_BuildSortByMenu(FMenuBuilder& MenuBuilder) { MenuBuilder.BeginSection("SortColumn", LOCTEXT("ContextMenu_Section_SortColumn", "Sort Column")); for (const TSharedRef& ColumnRef : Table->GetColumns()) { const FTableColumn& Column = *ColumnRef; if (Column.IsVisible() && Column.CanBeSorted()) { FUIAction Action_SortByColumn ( FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_SortByColumn_Execute, Column.GetId()), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_SortByColumn_CanExecute, Column.GetId()), FIsActionChecked::CreateSP(this, &SStatsView::ContextMenu_SortByColumn_IsChecked, Column.GetId()) ); MenuBuilder.AddMenuEntry ( Column.GetTitleName(), Column.GetDescription(), FSlateIcon(), Action_SortByColumn, NAME_None, EUserInterfaceActionType::RadioButton ); } } MenuBuilder.EndSection(); MenuBuilder.BeginSection("SortMode", LOCTEXT("ContextMenu_Section_SortMode", "Sort Mode")); { FUIAction Action_SortAscending ( FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_SortMode_Execute, EColumnSortMode::Ascending), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_SortMode_CanExecute, EColumnSortMode::Ascending), FIsActionChecked::CreateSP(this, &SStatsView::ContextMenu_SortMode_IsChecked, EColumnSortMode::Ascending) ); MenuBuilder.AddMenuEntry ( LOCTEXT("ContextMenu_SortAscending", "Sort Ascending"), LOCTEXT("ContextMenu_SortAscending_Desc", "Sorts ascending."), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.SortUp"), Action_SortAscending, NAME_None, EUserInterfaceActionType::RadioButton ); FUIAction Action_SortDescending ( FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_SortMode_Execute, EColumnSortMode::Descending), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_SortMode_CanExecute, EColumnSortMode::Descending), FIsActionChecked::CreateSP(this, &SStatsView::ContextMenu_SortMode_IsChecked, EColumnSortMode::Descending) ); MenuBuilder.AddMenuEntry ( LOCTEXT("ContextMenu_SortDescending", "Sort Descending"), LOCTEXT("ContextMenu_SortDescending_Desc", "Sorts descending."), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.SortDown"), Action_SortDescending, NAME_None, EUserInterfaceActionType::RadioButton ); } MenuBuilder.EndSection(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::TreeView_BuildViewColumnMenu(FMenuBuilder& MenuBuilder) { MenuBuilder.BeginSection("Columns", LOCTEXT("ContextMenu_Section_Columns", "Columns")); for (const TSharedRef& ColumnRef : Table->GetColumns()) { const FTableColumn& Column = *ColumnRef; FUIAction Action_ToggleColumn ( FExecuteAction::CreateSP(this, &SStatsView::ToggleColumnVisibility, Column.GetId()), FCanExecuteAction::CreateSP(this, &SStatsView::CanToggleColumnVisibility, Column.GetId()), FIsActionChecked::CreateSP(this, &SStatsView::IsColumnVisible, Column.GetId()) ); MenuBuilder.AddMenuEntry ( Column.GetTitleName(), Column.GetDescription(), FSlateIcon(), Action_ToggleColumn, NAME_None, EUserInterfaceActionType::ToggleButton ); } MenuBuilder.EndSection(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::InitializeAndShowHeaderColumns() { // Create columns. TArray> Columns; FStatsViewColumnFactory::CreateStatsViewColumns(Columns); Table->SetColumns(Columns); // Show columns. for (const TSharedRef& ColumnRef : Table->GetColumns()) { if (ColumnRef->ShouldBeVisible()) { ShowColumn(ColumnRef->GetId()); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// FText SStatsView::GetColumnHeaderText(const FName ColumnId) const { const FTableColumn& Column = *Table->FindColumnChecked(ColumnId); return Column.GetShortName(); } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedRef SStatsView::TreeViewHeaderRow_GenerateColumnMenu(const FTableColumn& Column) { const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, NULL); MenuBuilder.BeginSection("Sorting", LOCTEXT("ContextMenu_Section_Sorting", "Sorting")); { if (Column.CanBeSorted()) { FUIAction Action_SortAscending ( FExecuteAction::CreateSP(this, &SStatsView::HeaderMenu_SortMode_Execute, Column.GetId(), EColumnSortMode::Ascending), FCanExecuteAction::CreateSP(this, &SStatsView::HeaderMenu_SortMode_CanExecute, Column.GetId(), EColumnSortMode::Ascending), FIsActionChecked::CreateSP(this, &SStatsView::HeaderMenu_SortMode_IsChecked, Column.GetId(), EColumnSortMode::Ascending) ); MenuBuilder.AddMenuEntry ( FText::Format(LOCTEXT("ContextMenu_SortAscending_Fmt", "Sort Ascending (by {0})"), Column.GetTitleName()), FText::Format(LOCTEXT("ContextMenu_SortAscending_Desc_Fmt", "Sorts ascending by {0}."), Column.GetTitleName()), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.SortUp"), Action_SortAscending, NAME_None, EUserInterfaceActionType::RadioButton ); FUIAction Action_SortDescending ( FExecuteAction::CreateSP(this, &SStatsView::HeaderMenu_SortMode_Execute, Column.GetId(), EColumnSortMode::Descending), FCanExecuteAction::CreateSP(this, &SStatsView::HeaderMenu_SortMode_CanExecute, Column.GetId(), EColumnSortMode::Descending), FIsActionChecked::CreateSP(this, &SStatsView::HeaderMenu_SortMode_IsChecked, Column.GetId(), EColumnSortMode::Descending) ); MenuBuilder.AddMenuEntry ( FText::Format(LOCTEXT("ContextMenu_SortDescending_Fmt", "Sort Descending (by {0})"), Column.GetTitleName()), FText::Format(LOCTEXT("ContextMenu_SortDescending_Desc_Fmt", "Sorts descending by {0}."), Column.GetTitleName()), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "Icons.SortDown"), Action_SortDescending, NAME_None, EUserInterfaceActionType::RadioButton ); } MenuBuilder.AddSubMenu ( LOCTEXT("ContextMenu_SortBy_SubMenu", "Sort By"), LOCTEXT("ContextMenu_SortBy_SubMenu_Desc", "Sorts by a column."), FNewMenuDelegate::CreateSP(this, &SStatsView::TreeView_BuildSortByMenu), false, FSlateIcon(FInsightsStyle::GetStyleSetName(), "Icons.SortBy") ); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("ColumnVisibility", LOCTEXT("ContextMenu_Section_ColumnVisibility", "Column Visibility")); { if (Column.CanBeHidden()) { FUIAction Action_HideColumn ( FExecuteAction::CreateSP(this, &SStatsView::HideColumn, Column.GetId()), FCanExecuteAction::CreateSP(this, &SStatsView::CanHideColumn, Column.GetId()) ); MenuBuilder.AddMenuEntry ( LOCTEXT("ContextMenu_HideColumn", "Hide"), LOCTEXT("ContextMenu_HideColumn_Desc", "Hides the selected column."), FSlateIcon(), Action_HideColumn, NAME_None, EUserInterfaceActionType::Button ); } MenuBuilder.AddSubMenu ( LOCTEXT("ContextMenu_ViewColumn_SubMenu", "View Column"), LOCTEXT("ContextMenu_ViewColumn_SubMenu_Desc", "Hides or shows columns."), FNewMenuDelegate::CreateSP(this, &SStatsView::TreeView_BuildViewColumnMenu), false, FSlateIcon(FInsightsStyle::GetStyleSetName(), "Icons.ViewColumn") ); FUIAction Action_ShowAllColumns ( FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ShowAllColumns_Execute), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ShowAllColumns_CanExecute) ); MenuBuilder.AddMenuEntry ( LOCTEXT("ContextMenu_ShowAllColumns", "Show All Columns"), LOCTEXT("ContextMenu_ShowAllColumns_Desc", "Resets tree view to show all columns."), FSlateIcon(FInsightsStyle::GetStyleSetName(), "Icons.ResetColumn"), Action_ShowAllColumns, NAME_None, EUserInterfaceActionType::Button ); FUIAction Action_ShowMinMaxMedColumns ( FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ShowMinMaxMedColumns_Execute), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ShowMinMaxMedColumns_CanExecute) ); MenuBuilder.AddMenuEntry ( LOCTEXT("ContextMenu_ShowMinMaxMedColumns", "Reset Columns to Min/Max/Median Preset"), LOCTEXT("ContextMenu_ShowMinMaxMedColumns_Desc", "Resets columns to Min/Max/Median preset."), FSlateIcon(FInsightsStyle::GetStyleSetName(), "Icons.ResetColumn"), Action_ShowMinMaxMedColumns, NAME_None, EUserInterfaceActionType::Button ); FUIAction Action_ResetColumns ( FExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ResetColumns_Execute), FCanExecuteAction::CreateSP(this, &SStatsView::ContextMenu_ResetColumns_CanExecute) ); MenuBuilder.AddMenuEntry ( LOCTEXT("ContextMenu_ResetColumns", "Reset Columns to Default"), LOCTEXT("ContextMenu_ResetColumns_Desc", "Resets columns to default."), FSlateIcon(FInsightsStyle::GetStyleSetName(), "Icons.ResetColumn"), Action_ResetColumns, NAME_None, EUserInterfaceActionType::Button ); } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::InsightsManager_OnSessionChanged() { TSharedPtr NewSession = FInsightsManager::Get()->GetSession(); if (NewSession != Session) { Session = NewSession; Reset(); } else { UpdateTree(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::InsightsManager_OnSessionAnalysisCompleted() { // Re-sync the list of counters to update the "" counter names. RebuildTree(true); // Aggregate stats automatically for the entire session (but only if user didn't made a time selection yet). if (Aggregator->IsEmptyTimeInterval() && !Aggregator->IsRunning()) { TSharedPtr InsightsManager = FInsightsManager::Get(); InsightsManager->UpdateSessionDuration(); const double SessionDuration = InsightsManager->GetSessionDuration(); constexpr double Delta = 0.0; // session padding Aggregator->Cancel(); Aggregator->SetTimeInterval(0.0 - Delta, SessionDuration + Delta); Aggregator->Start(); if (ColumnBeingSorted == NAME_None) { // Restore sorting... SetSortModeForColumn(GetDefaultColumnBeingSorted(), GetDefaultColumnSortMode()); TreeView_Refresh(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::UpdateTree() { CreateGroups(); SortTreeNodes(); ApplyFiltering(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ApplyFiltering() { FStopwatch Stopwatch; Stopwatch.Start(); FilteredGroupNodes.Reset(); // Apply filter to all groups and its children. const int32 NumGroups = GroupNodes.Num(); for (int32 GroupIndex = 0; GroupIndex < NumGroups; ++GroupIndex) { FStatsNodePtr& GroupPtr = GroupNodes[GroupIndex]; GroupPtr->ClearFilteredChildren(); const bool bIsGroupVisible = Filters->PassesAllFilters(GroupPtr); const TArray& GroupChildren = GroupPtr->GetChildren(); int32 NumVisibleChildren = 0; for (const FBaseTreeNodePtr& ChildPtr : GroupChildren) { const FStatsNodePtr& NodePtr = StaticCastSharedPtr(ChildPtr); const bool bIsChildVisible = (!bFilterOutZeroCountStats || NodePtr->GetAggregatedStats().Count > 0) && FilterByNodeType[static_cast(NodePtr->GetType())] && FilterByDataType[static_cast(NodePtr->GetDataType())] && Filters->PassesAllFilters(NodePtr); if (bIsChildVisible) { // Add a child. GroupPtr->AddFilteredChild(NodePtr); NumVisibleChildren++; } } if (bIsGroupVisible || NumVisibleChildren > 0) { // Add a group. FilteredGroupNodes.Add(GroupPtr); GroupPtr->SetExpansion(true); } else { GroupPtr->SetExpansion(false); } } // Only expand tree nodes if we have a text filter. const bool bNonEmptyTextFilter = !TextFilter->GetRawFilterText().IsEmpty(); if (bNonEmptyTextFilter) { if (!bExpansionSaved) { ExpandedNodes.Empty(); TreeView->GetExpandedItems(ExpandedNodes); bExpansionSaved = true; } for (const FStatsNodePtr& GroupPtr : FilteredGroupNodes) { TreeView->SetItemExpansion(GroupPtr, GroupPtr->IsExpanded()); } } else { if (bExpansionSaved) { // Restore previously expanded nodes when the text filter is disabled. TreeView->ClearExpandedItems(); for (auto It = ExpandedNodes.CreateConstIterator(); It; ++It) { TreeView->SetItemExpansion(*It, true); } bExpansionSaved = false; } } // Update aggregations for groups. for (FStatsNodePtr& GroupPtr : FilteredGroupNodes) { FAggregatedStats& AggregatedStats = GroupPtr->GetAggregatedStats(); GroupPtr->ResetAggregatedStats(); AggregatedStats.DoubleStats.Min = std::numeric_limits::max(); AggregatedStats.DoubleStats.Max = std::numeric_limits::lowest(); constexpr double NotAvailableDoubleValue = std::numeric_limits::quiet_NaN(); AggregatedStats.DoubleStats.Average = NotAvailableDoubleValue; AggregatedStats.DoubleStats.Median = NotAvailableDoubleValue; AggregatedStats.DoubleStats.LowerQuartile = NotAvailableDoubleValue; AggregatedStats.DoubleStats.UpperQuartile = NotAvailableDoubleValue; AggregatedStats.Int64Stats.Min = std::numeric_limits::max(); AggregatedStats.Int64Stats.Max = std::numeric_limits::lowest(); constexpr int64 NotAvailableIntegerValue = 0;// std::numeric_limits::max(); AggregatedStats.Int64Stats.Average = NotAvailableIntegerValue; AggregatedStats.Int64Stats.Median = NotAvailableIntegerValue; AggregatedStats.Int64Stats.LowerQuartile = NotAvailableIntegerValue; AggregatedStats.Int64Stats.UpperQuartile = NotAvailableIntegerValue; EStatsNodeDataType GroupDataType = EStatsNodeDataType::InvalidOrMax; const TArray& GroupChildren = GroupPtr->GetFilteredChildren(); for (const FBaseTreeNodePtr& ChildPtr : GroupChildren) { const FStatsNodePtr& NodePtr = StaticCastSharedPtr(ChildPtr); const FAggregatedStats& NodeAggregatedStats = NodePtr->GetAggregatedStats(); if (NodeAggregatedStats.Count > 0) { AggregatedStats.Count += NodeAggregatedStats.Count; AggregatedStats.DoubleStats.Sum += NodeAggregatedStats.DoubleStats.Sum; AggregatedStats.DoubleStats.Min = FMath::Min(AggregatedStats.DoubleStats.Min, NodeAggregatedStats.DoubleStats.Min); AggregatedStats.DoubleStats.Max = FMath::Max(AggregatedStats.DoubleStats.Max, NodeAggregatedStats.DoubleStats.Max); AggregatedStats.Int64Stats.Sum += NodeAggregatedStats.Int64Stats.Sum; AggregatedStats.Int64Stats.Min = FMath::Min(AggregatedStats.Int64Stats.Min, NodeAggregatedStats.Int64Stats.Min); AggregatedStats.Int64Stats.Max = FMath::Max(AggregatedStats.Int64Stats.Max, NodeAggregatedStats.Int64Stats.Max); } if (GroupDataType == EStatsNodeDataType::InvalidOrMax) { GroupDataType = NodePtr->GetDataType(); } else { if (GroupDataType != NodePtr->GetDataType()) { GroupDataType = EStatsNodeDataType::Undefined; } } } // If not all children have same type, reset aggregated stats for group. if (GroupDataType >= EStatsNodeDataType::Undefined) { AggregatedStats.DoubleStats.Sum = NotAvailableDoubleValue; AggregatedStats.DoubleStats.Min = NotAvailableDoubleValue; AggregatedStats.DoubleStats.Max = NotAvailableDoubleValue; AggregatedStats.Int64Stats.Sum = NotAvailableIntegerValue; AggregatedStats.Int64Stats.Min = NotAvailableIntegerValue; AggregatedStats.Int64Stats.Max = NotAvailableIntegerValue; } GroupPtr->SetDataType(GroupDataType); } // Request tree refresh TreeView->RequestTreeRefresh(); Stopwatch.Stop(); const double TotalTime = Stopwatch.GetAccumulatedTime(); if (TotalTime > 0.1) { UE_LOG(LogTimingProfiler, Log, TEXT("[Counters] Tree view filtered in %.3fs (%d counters)"), TotalTime, StatsNodes.Num()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::HandleItemToStringArray(const FStatsNodePtr& FStatsNodePtr, TArray& OutSearchStrings) const { OutSearchStrings.Add(FStatsNodePtr->GetName().GetPlainNameString()); } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedRef SStatsView::GetToggleButtonForNodeType(const EStatsNodeType NodeType) { return SNew(SCheckBox) .Style(FAppStyle::Get(), "ToggleButtonCheckbox") .Padding(FMargin(4.0f, 2.0f, 4.0f, 2.0f)) .HAlign(HAlign_Center) .OnCheckStateChanged(this, &SStatsView::FilterByStatsType_OnCheckStateChanged, NodeType) .IsChecked(this, &SStatsView::FilterByStatsType_IsChecked, NodeType) .ToolTipText(StatsNodeTypeHelper::ToDescription(NodeType)) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(0.0f, 0.0f, 2.0f, 0.0f) [ SNew(SImage) .Image(StatsNodeTypeHelper::GetIcon(NodeType)) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(StatsNodeTypeHelper::ToText(NodeType)) ] ]; } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedRef SStatsView::GetToggleButtonForDataType(const EStatsNodeDataType DataType) { return SNew(SCheckBox) .Style(FAppStyle::Get(), "ToggleButtonCheckbox") .Padding(FMargin(4.0f, 2.0f, 4.0f, 2.0f)) .HAlign(HAlign_Center) .OnCheckStateChanged(this, &SStatsView::FilterByStatsDataType_OnCheckStateChanged, DataType) .IsChecked(this, &SStatsView::FilterByStatsDataType_IsChecked, DataType) .ToolTipText(StatsNodeDataTypeHelper::ToDescription(DataType)) [ SNew(SHorizontalBox) //+ SHorizontalBox::Slot() //.Padding(0.0f, 0.0f, 2.0f, 0.0f) //.AutoWidth() //.VAlign(VAlign_Center) //[ // SNew(SImage) // .Image(StatsNodeDataTypeHelper::GetIcon(DataType)) //] + SHorizontalBox::Slot() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(StatsNodeDataTypeHelper::ToText(DataType)) ] ]; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::FilterOutZeroCountStats_OnCheckStateChanged(ECheckBoxState NewRadioState) { bFilterOutZeroCountStats = (NewRadioState == ECheckBoxState::Checked); ApplyFiltering(); } //////////////////////////////////////////////////////////////////////////////////////////////////// ECheckBoxState SStatsView::FilterOutZeroCountStats_IsChecked() const { return bFilterOutZeroCountStats ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::FilterByStatsType_OnCheckStateChanged(ECheckBoxState NewRadioState, const EStatsNodeType InNodeType) { FilterByNodeType[static_cast(InNodeType)] = (NewRadioState == ECheckBoxState::Checked); ApplyFiltering(); } //////////////////////////////////////////////////////////////////////////////////////////////////// ECheckBoxState SStatsView::FilterByStatsType_IsChecked(const EStatsNodeType InNodeType) const { return FilterByNodeType[static_cast(InNodeType)] ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::FilterByStatsDataType_OnCheckStateChanged(ECheckBoxState NewRadioState, const EStatsNodeDataType InDataType) { FilterByDataType[static_cast(InDataType)] = (NewRadioState == ECheckBoxState::Checked); ApplyFiltering(); } //////////////////////////////////////////////////////////////////////////////////////////////////// ECheckBoxState SStatsView::FilterByStatsDataType_IsChecked(const EStatsNodeDataType InDataType) const { return FilterByDataType[static_cast(InDataType)] ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } //////////////////////////////////////////////////////////////////////////////////////////////////// // TreeView //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::TreeView_Refresh() { if (TreeView.IsValid()) { TreeView->RequestTreeRefresh(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::TreeView_OnSelectionChanged(FStatsNodePtr SelectedItem, ESelectInfo::Type SelectInfo) { if (SelectInfo != ESelectInfo::Direct) { //TArray SelectedItems = TreeView->GetSelectedItems(); //if (SelectedItems.Num() == 1 && SelectedItems[0]->GetType() != EStatsNodeType::Group) //{ // FTimingProfilerManager::Get()->SetSelectedCounter(SelectedItems[0]->GetCounterId()); //} } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::TreeView_OnGetChildren(FStatsNodePtr InParent, TArray& OutChildren) { const TArray& Children = InParent->GetFilteredChildren(); OutChildren.Reset(Children.Num()); for (const FBaseTreeNodePtr& Child : Children) { OutChildren.Add(StaticCastSharedPtr(Child)); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::TreeView_OnMouseButtonDoubleClick(FStatsNodePtr NodePtr) { if (NodePtr->GetType() == EStatsNodeType::Group) { const bool bIsGroupExpanded = TreeView->IsItemExpanded(NodePtr); TreeView->SetItemExpansion(NodePtr, !bIsGroupExpanded); } else { ToggleTimingViewMainGraphEventSeries(NodePtr); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // Tree View's Table Row //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedRef SStatsView::TreeView_OnGenerateRow(FStatsNodePtr NodePtr, const TSharedRef& OwnerTable) { TSharedRef TableRow = SNew(SStatsTableRow, OwnerTable) .OnShouldBeEnabled(this, &SStatsView::TableRow_ShouldBeEnabled) .OnIsColumnVisible(this, &SStatsView::IsColumnVisible) .OnSetHoveredCell(this, &SStatsView::TableRow_SetHoveredCell) .OnGetColumnOutlineHAlignmentDelegate(this, &SStatsView::TableRow_GetColumnOutlineHAlignment) .HighlightText(this, &SStatsView::TableRow_GetHighlightText) .HighlightedNodeName(this, &SStatsView::TableRow_GetHighlightedNodeName) .TablePtr(Table) .StatsNodePtr(NodePtr); return TableRow; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::TableRow_ShouldBeEnabled(FStatsNodePtr NodePtr) const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::TableRow_SetHoveredCell(TSharedPtr InTablePtr, TSharedPtr InColumnPtr, FStatsNodePtr InNodePtr) { HoveredColumnId = InColumnPtr ? InColumnPtr->GetId() : FName(); const bool bIsAnyMenusVisible = FSlateApplication::Get().AnyMenusVisible(); if (!HasMouseCapture() && !bIsAnyMenusVisible) { HoveredNodePtr = InNodePtr; } } //////////////////////////////////////////////////////////////////////////////////////////////////// EHorizontalAlignment SStatsView::TableRow_GetColumnOutlineHAlignment(const FName ColumnId) const { const TIndirectArray& Columns = TreeViewHeaderRow->GetColumns(); const int32 LastColumnIdx = Columns.Num() - 1; // First column if (Columns[0].ColumnId == ColumnId) { return HAlign_Left; } // Last column else if (Columns[LastColumnIdx].ColumnId == ColumnId) { return HAlign_Right; } // Middle columns { return HAlign_Center; } } //////////////////////////////////////////////////////////////////////////////////////////////////// FText SStatsView::TableRow_GetHighlightText() const { return SearchBox->GetText(); } //////////////////////////////////////////////////////////////////////////////////////////////////// FName SStatsView::TableRow_GetHighlightedNodeName() const { return HighlightedNodeName; } //////////////////////////////////////////////////////////////////////////////////////////////////// // SearchBox //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::SearchBox_OnTextChanged(const FText& InFilterText) { TextFilter->SetRawFilterText(InFilterText); SearchBox->SetError(TextFilter->GetFilterErrorText()); ApplyFiltering(); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::SearchBox_IsEnabled() const { return StatsNodes.Num() > 0; } //////////////////////////////////////////////////////////////////////////////////////////////////// // Grouping //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::CreateGroups() { FStopwatch Stopwatch; Stopwatch.Start(); if (GroupingMode == EStatsGroupingMode::Flat) { GroupNodes.Reset(); const FName GroupName(TEXT("All")); FStatsNodePtr GroupPtr = MakeShared(GroupName); GroupNodes.Add(GroupPtr); for (const FStatsNodePtr& NodePtr : StatsNodes) { GroupPtr->AddChildAndSetParent(NodePtr); } TreeView->SetItemExpansion(GroupPtr, true); } // Creates groups based on stat metadata groups. else if (GroupingMode == EStatsGroupingMode::ByMetaGroupName) { TMap GroupNodeSet; for (const FStatsNodePtr& NodePtr : StatsNodes) { const FName GroupName = NodePtr->GetMetaGroupName(); FStatsNodePtr GroupPtr = GroupNodeSet.FindRef(GroupName); if (!GroupPtr) { GroupPtr = GroupNodeSet.Add(GroupName, MakeShared(GroupName)); } GroupPtr->AddChildAndSetParent(NodePtr); TreeView->SetItemExpansion(GroupPtr, true); } GroupNodeSet.KeySort([](const FName& A, const FName& B) { return A.Compare(B) < 0; }); // sort groups by name GroupNodeSet.GenerateValueArray(GroupNodes); } // Creates one group for each node type. else if (GroupingMode == EStatsGroupingMode::ByType) { TMap GroupNodeSet; for (const FStatsNodePtr& NodePtr : StatsNodes) { const EStatsNodeType NodeType = NodePtr->GetType(); FStatsNodePtr GroupPtr = GroupNodeSet.FindRef(NodeType); if (!GroupPtr) { const FName GroupName = *StatsNodeTypeHelper::ToText(NodeType).ToString(); GroupPtr = GroupNodeSet.Add(NodeType, MakeShared(GroupName)); } GroupPtr->AddChildAndSetParent(NodePtr); TreeView->SetItemExpansion(GroupPtr, true); } GroupNodeSet.KeySort([](const EStatsNodeType& A, const EStatsNodeType& B) { return A < B; }); // sort groups by type GroupNodeSet.GenerateValueArray(GroupNodes); } // Creates one group for each data type. else if (GroupingMode == EStatsGroupingMode::ByDataType) { TMap GroupNodeSet; for (const FStatsNodePtr& NodePtr : StatsNodes) { const EStatsNodeDataType DataType = NodePtr->GetDataType(); FStatsNodePtr GroupPtr = GroupNodeSet.FindRef(DataType); if (!GroupPtr) { const FName GroupName = *StatsNodeDataTypeHelper::ToText(DataType).ToString(); GroupPtr = GroupNodeSet.Add(DataType, MakeShared(GroupName)); } GroupPtr->AddChildAndSetParent(NodePtr); TreeView->SetItemExpansion(GroupPtr, true); } GroupNodeSet.KeySort([](const EStatsNodeDataType& A, const EStatsNodeDataType& B) { return A < B; }); // sort groups by data type GroupNodeSet.GenerateValueArray(GroupNodes); } // Creates one group for one letter. else if (GroupingMode == EStatsGroupingMode::ByName) { TMap GroupNodeSet; for (const FStatsNodePtr& NodePtr : StatsNodes) { FString FirstLetterStr(NodePtr->GetName().GetPlainNameString().Left(1).ToUpper()); const TCHAR FirstLetter = FirstLetterStr[0]; FStatsNodePtr GroupPtr = GroupNodeSet.FindRef(FirstLetter); if (!GroupPtr) { const FName GroupName(FirstLetterStr); GroupPtr = GroupNodeSet.Add(FirstLetter, MakeShared(GroupName)); } GroupPtr->AddChildAndSetParent(NodePtr); } GroupNodeSet.KeySort([](const TCHAR& A, const TCHAR& B) { return A < B; }); // sort groups alphabetically GroupNodeSet.GenerateValueArray(GroupNodes); } // Creates one group for each logarithmic range ie. 0, [1 .. 10), [10 .. 100), [100 .. 1K), etc. else if (GroupingMode == EStatsGroupingMode::ByCount) { const TCHAR* Orders[] = { TEXT("1"), TEXT("10"), TEXT("100"), TEXT("1K"), TEXT("10K"), TEXT("100K"), TEXT("1M"), TEXT("10M"), TEXT("100M"), TEXT("1G"), TEXT("10G"), TEXT("100G"), TEXT("1T") }; const uint32 MaxOrder = UE_ARRAY_COUNT(Orders); TMap GroupNodeSet; for (const FStatsNodePtr& NodePtr : StatsNodes) { uint64 InstanceCount = NodePtr->GetAggregatedStats().Count; uint32 Order = 0; while (InstanceCount) { InstanceCount /= 10; Order++; } FStatsNodePtr GroupPtr = GroupNodeSet.FindRef(Order); if (!GroupPtr) { const FName GroupName = (Order == 0) ? FName(TEXT("Count == 0")) : (Order < MaxOrder) ? FName(FString::Printf(TEXT("Count: [%s .. %s)"), Orders[Order - 1], Orders[Order])) : FName(FString::Printf(TEXT("Count >= %s"), Orders[MaxOrder - 1])); GroupPtr = GroupNodeSet.Add(Order, MakeShared(GroupName)); } GroupPtr->AddChildAndSetParent(NodePtr); } GroupNodeSet.KeySort([](const uint32& A, const uint32& B) { return A > B; }); // sort groups by order GroupNodeSet.GenerateValueArray(GroupNodes); } Stopwatch.Stop(); const double TotalTime = Stopwatch.GetAccumulatedTime(); if (TotalTime > 0.1) { UE_LOG(LogTimingProfiler, Log, TEXT("[Counters] Tree view grouping updated in %.3fs (%d counters)"), TotalTime, StatsNodes.Num()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::CreateGroupByOptionsSources() { GroupByOptionsSource.Reset(3); // Must be added in order of elements in the EStatsGroupingMode. GroupByOptionsSource.Add(MakeShared(EStatsGroupingMode::Flat)); GroupByOptionsSource.Add(MakeShared(EStatsGroupingMode::ByName)); GroupByOptionsSource.Add(MakeShared(EStatsGroupingMode::ByMetaGroupName)); GroupByOptionsSource.Add(MakeShared(EStatsGroupingMode::ByType)); GroupByOptionsSource.Add(MakeShared(EStatsGroupingMode::ByDataType)); GroupByOptionsSource.Add(MakeShared(EStatsGroupingMode::ByCount)); EStatsGroupingModePtr* GroupingModePtrPtr = GroupByOptionsSource.FindByPredicate([&](const EStatsGroupingModePtr InGroupingModePtr) { return *InGroupingModePtr == GroupingMode; }); if (GroupingModePtrPtr != nullptr) { GroupByComboBox->SetSelectedItem(*GroupingModePtrPtr); } GroupByComboBox->RefreshOptions(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::GroupBy_OnSelectionChanged(TSharedPtr NewGroupingMode, ESelectInfo::Type SelectInfo) { if (SelectInfo != ESelectInfo::Direct) { GroupingMode = *NewGroupingMode; CreateGroups(); SortTreeNodes(); ApplyFiltering(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedRef SStatsView::GroupBy_OnGenerateWidget(TSharedPtr InGroupingMode) const { return SNew(STextBlock) .Text(StatsNodeGroupingHelper::ToText(*InGroupingMode)) .ToolTipText(StatsNodeGroupingHelper::ToDescription(*InGroupingMode)); } //////////////////////////////////////////////////////////////////////////////////////////////////// FText SStatsView::GroupBy_GetSelectedText() const { return StatsNodeGroupingHelper::ToText(GroupingMode); } //////////////////////////////////////////////////////////////////////////////////////////////////// FText SStatsView::GroupBy_GetSelectedTooltipText() const { return StatsNodeGroupingHelper::ToDescription(GroupingMode); } //////////////////////////////////////////////////////////////////////////////////////////////////// // Sorting //////////////////////////////////////////////////////////////////////////////////////////////////// const FName SStatsView::GetDefaultColumnBeingSorted() { return FStatsViewColumns::NameColumnID; } //////////////////////////////////////////////////////////////////////////////////////////////////// const EColumnSortMode::Type SStatsView::GetDefaultColumnSortMode() { return EColumnSortMode::Ascending; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::CreateSortings() { AvailableSorters.Reset(); CurrentSorter = nullptr; for (const TSharedRef& ColumnRef : Table->GetColumns()) { if (ColumnRef->CanBeSorted()) { TSharedPtr SorterPtr = ColumnRef->GetValueSorter(); if (ensure(SorterPtr.IsValid())) { AvailableSorters.Add(SorterPtr); } } } UpdateCurrentSortingByColumn(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::UpdateCurrentSortingByColumn() { TSharedPtr ColumnPtr = Table->FindColumn(ColumnBeingSorted); CurrentSorter = ColumnPtr.IsValid() ? ColumnPtr->GetValueSorter() : nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::SortTreeNodes() { FStopwatch Stopwatch; Stopwatch.Start(); if (CurrentSorter.IsValid()) { for (FStatsNodePtr& Root : GroupNodes) { SortTreeNodesRec(*Root, *CurrentSorter); } } Stopwatch.Stop(); const double TotalTime = Stopwatch.GetAccumulatedTime(); if (TotalTime > 0.1) { UE_LOG(LogTimingProfiler, Log, TEXT("[Counters] Tree view sorted (%s, %c) in %.3fs (%d counters)"), CurrentSorter.IsValid() ? *CurrentSorter->GetShortName().ToString() : TEXT("N/A"), (ColumnSortMode == EColumnSortMode::Type::Descending) ? TEXT('D') : TEXT('A'), TotalTime, StatsNodes.Num()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::SortTreeNodesRec(FStatsNode& Node, const ITableCellValueSorter& Sorter) { ESortMode SortMode = (ColumnSortMode == EColumnSortMode::Type::Descending) ? ESortMode::Descending : ESortMode::Ascending; Node.SortChildren(Sorter, SortMode); #if 0 // Current groupings creates only one level. for (FBaseTreeNodePtr ChildPtr : Node.GetChildren()) { if (ChildPtr->GetChildrenCount() > 0) { SortTreeNodesRec(*StaticCastSharedPtr(ChildPtr), Sorter); } } #endif } //////////////////////////////////////////////////////////////////////////////////////////////////// EColumnSortMode::Type SStatsView::GetSortModeForColumn(const FName ColumnId) const { if (ColumnBeingSorted != ColumnId) { return EColumnSortMode::None; } return ColumnSortMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::SetSortModeForColumn(const FName& ColumnId, const EColumnSortMode::Type SortMode) { ColumnBeingSorted = ColumnId; ColumnSortMode = SortMode; UpdateCurrentSortingByColumn(); SortTreeNodes(); ApplyFiltering(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::OnSortModeChanged(const EColumnSortPriority::Type SortPriority, const FName& ColumnId, const EColumnSortMode::Type SortMode) { SetSortModeForColumn(ColumnId, SortMode); TreeView_Refresh(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // SortMode action (HeaderMenu) //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::HeaderMenu_SortMode_IsChecked(const FName ColumnId, const EColumnSortMode::Type InSortMode) { return ColumnBeingSorted == ColumnId && ColumnSortMode == InSortMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::HeaderMenu_SortMode_CanExecute(const FName ColumnId, const EColumnSortMode::Type InSortMode) const { const FTableColumn& Column = *Table->FindColumnChecked(ColumnId); return Column.CanBeSorted(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::HeaderMenu_SortMode_Execute(const FName ColumnId, const EColumnSortMode::Type InSortMode) { SetSortModeForColumn(ColumnId, InSortMode); TreeView_Refresh(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // SortMode action (ContextMenu) //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_SortMode_IsChecked(const EColumnSortMode::Type InSortMode) { return ColumnSortMode == InSortMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_SortMode_CanExecute(const EColumnSortMode::Type InSortMode) const { return true; //ColumnSortMode != InSortMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_SortMode_Execute(const EColumnSortMode::Type InSortMode) { SetSortModeForColumn(ColumnBeingSorted, InSortMode); TreeView_Refresh(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // SortByColumn action (ContextMenu) //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_SortByColumn_IsChecked(const FName ColumnId) { return ColumnId == ColumnBeingSorted; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_SortByColumn_CanExecute(const FName ColumnId) const { return true; //ColumnId != ColumnBeingSorted; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_SortByColumn_Execute(const FName ColumnId) { SetSortModeForColumn(ColumnId, EColumnSortMode::Descending); TreeView_Refresh(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // ShowColumn action //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::CanShowColumn(const FName ColumnId) const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ShowColumn(const FName ColumnId) { FTableColumn& Column = *Table->FindColumnChecked(ColumnId); Column.Show(); SHeaderRow::FColumn::FArguments ColumnArgs; ColumnArgs .ColumnId(Column.GetId()) .DefaultLabel(Column.GetShortName()) .ToolTip(SStatsViewTooltip::GetColumnTooltip(Column)) .HAlignHeader(Column.GetHorizontalAlignment()) .VAlignHeader(VAlign_Center) .HAlignCell(HAlign_Fill) .VAlignCell(VAlign_Fill) .InitialSortMode(Column.GetInitialSortMode()) .SortMode(this, &SStatsView::GetSortModeForColumn, Column.GetId()) .OnSort(this, &SStatsView::OnSortModeChanged) .FillWidth(Column.GetInitialWidth()) //.FixedWidth(Column.IsFixedWidth() ? Column.GetInitialWidth() : TOptional()) .HeaderContent() [ SNew(SBox) .HeightOverride(24.0f) .Padding(FMargin(0.0f)) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(this, &SStatsView::GetColumnHeaderText, Column.GetId()) ] ] .MenuContent() [ TreeViewHeaderRow_GenerateColumnMenu(Column) ]; int32 ColumnIndex = 0; const int32 NewColumnPosition = Table->GetColumnPositionIndex(ColumnId); const int32 NumColumns = TreeViewHeaderRow->GetColumns().Num(); for (; ColumnIndex < NumColumns; ColumnIndex++) { const SHeaderRow::FColumn& CurrentColumn = TreeViewHeaderRow->GetColumns()[ColumnIndex]; const int32 CurrentColumnPosition = Table->GetColumnPositionIndex(CurrentColumn.ColumnId); if (NewColumnPosition < CurrentColumnPosition) { break; } } TreeViewHeaderRow->InsertColumn(ColumnArgs, ColumnIndex); } //////////////////////////////////////////////////////////////////////////////////////////////////// // HideColumn action //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::CanHideColumn(const FName ColumnId) const { const FTableColumn& Column = *Table->FindColumnChecked(ColumnId); return Column.CanBeHidden(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::HideColumn(const FName ColumnId) { FTableColumn& Column = *Table->FindColumnChecked(ColumnId); Column.Hide(); TreeViewHeaderRow->RemoveColumn(ColumnId); } //////////////////////////////////////////////////////////////////////////////////////////////////// // ToggleColumn action //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::IsColumnVisible(const FName ColumnId) const { const FTableColumn& Column = *Table->FindColumnChecked(ColumnId); return Column.IsVisible(); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::CanToggleColumnVisibility(const FName ColumnId) const { const FTableColumn& Column = *Table->FindColumnChecked(ColumnId); return !Column.IsVisible() || Column.CanBeHidden(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ToggleColumnVisibility(const FName ColumnId) { const FTableColumn& Column = *Table->FindColumnChecked(ColumnId); if (Column.IsVisible()) { HideColumn(ColumnId); } else { ShowColumn(ColumnId); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // "Show All Columns" action (ContextMenu) //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_ShowAllColumns_CanExecute() const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_ShowAllColumns_Execute() { ColumnBeingSorted = GetDefaultColumnBeingSorted(); ColumnSortMode = GetDefaultColumnSortMode(); UpdateCurrentSortingByColumn(); for (const TSharedRef& ColumnRef : Table->GetColumns()) { const FTableColumn& Column = *ColumnRef; if (!Column.IsVisible()) { ShowColumn(Column.GetId()); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// // "Show Min/Max/Median Columns" action (ContextMenu) //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_ShowMinMaxMedColumns_CanExecute() const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_ShowMinMaxMedColumns_Execute() { TSet Preset = { FStatsViewColumns::NameColumnID, //FStatsViewColumns::MetaGroupNameColumnID, //FStatsViewColumns::TypeColumnID, FStatsViewColumns::CountColumnID, FStatsViewColumns::SumColumnID, FStatsViewColumns::MaxColumnID, FStatsViewColumns::UpperQuartileColumnID, //FStatsViewColumns::AverageColumnID, FStatsViewColumns::MedianColumnID, FStatsViewColumns::LowerQuartileColumnID, FStatsViewColumns::MinColumnID, FStatsViewColumns::DiffColumnID, }; ColumnBeingSorted = FStatsViewColumns::CountColumnID; ColumnSortMode = EColumnSortMode::Descending; UpdateCurrentSortingByColumn(); for (const TSharedRef& ColumnRef : Table->GetColumns()) { const FTableColumn& Column = *ColumnRef; const bool bShouldBeVisible = Preset.Contains(Column.GetId()); if (bShouldBeVisible && !Column.IsVisible()) { ShowColumn(Column.GetId()); } else if (!bShouldBeVisible && Column.IsVisible()) { HideColumn(Column.GetId()); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// // ResetColumns action (ContextMenu) //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_ResetColumns_CanExecute() const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_ResetColumns_Execute() { ColumnBeingSorted = GetDefaultColumnBeingSorted(); ColumnSortMode = GetDefaultColumnSortMode(); UpdateCurrentSortingByColumn(); for (const TSharedRef& ColumnRef : Table->GetColumns()) { const FTableColumn& Column = *ColumnRef; if (Column.ShouldBeVisible() && !Column.IsVisible()) { ShowColumn(Column.GetId()); } else if (!Column.ShouldBeVisible() && Column.IsVisible()) { HideColumn(Column.GetId()); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::Reset() { Aggregator->Cancel(); Aggregator->SetTimeInterval(0.0, 0.0); RebuildTree(true); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { // Check if we need to update the list of counters, but not too often. static uint64 NextTimestamp = 0; const uint64 Time = FPlatformTime::Cycles64(); if (Time > NextTimestamp) { RebuildTree(false); // 1000 counters --> check each 150ms // 10000 counters --> check each 600ms // 100000 counters --> check each 5.1s const double WaitTimeSec = 0.1 + StatsNodes.Num() / 20000.0; const uint64 WaitTime = static_cast(WaitTimeSec / FPlatformTime::GetSecondsPerCycle64()); NextTimestamp = Time + WaitTime; } Aggregator->Tick(Session, InCurrentTime, InDeltaTime, [this]() { FinishAggregation(); }); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::RebuildTree(bool bResync) { FStopwatch Stopwatch; Stopwatch.Start(); FStopwatch SyncStopwatch; SyncStopwatch.Start(); if (bResync) { StatsNodes.Empty(); StatsNodesIdMap.Empty(); } const uint32 PreviousNodeCount = StatsNodes.Num(); if (Session.IsValid()) { TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const TraceServices::ICounterProvider& CountersProvider = TraceServices::ReadCounterProvider(*Session.Get()); const uint32 CounterCount = static_cast(CountersProvider.GetCounterCount()); if (CounterCount != PreviousNodeCount) { check(CounterCount > PreviousNodeCount); StatsNodes.Reserve(CounterCount); StatsNodesIdMap.Reserve(CounterCount); const FName MemoryGroup(TEXT("Memory")); const FName MiscFloatGroup(TEXT("Misc_double")); const FName MiscInt64Group(TEXT("Misc_int64")); // Add nodes only for new counters. uint32 CounterIndex = PreviousNodeCount; CountersProvider.EnumerateCounters([this, &CounterIndex, MemoryGroup, MiscFloatGroup, MiscInt64Group](uint32 CounterId, const TraceServices::ICounter& Counter) { FStatsNodePtr NodePtr = StatsNodesIdMap.FindRef(CounterId); if (!NodePtr) { FName Name(Counter.GetName()); const FName Group = Counter.GetGroup() ? Counter.GetGroup() : ((Counter.GetDisplayHint() == TraceServices::CounterDisplayHint_Memory) ? MemoryGroup : Counter.IsFloatingPoint() ? MiscFloatGroup : MiscInt64Group); const EStatsNodeType Type = EStatsNodeType::Counter; const EStatsNodeDataType DataType = Counter.IsFloatingPoint() ? EStatsNodeDataType::Double : EStatsNodeDataType::Int64; NodePtr = MakeShared(CounterId, Name, Group, Type, DataType); NodePtr->SetDefaultSortOrder(++CounterIndex); UpdateNode(NodePtr); StatsNodes.Add(NodePtr); StatsNodesIdMap.Add(CounterId, NodePtr); } }); ensure(StatsNodes.Num() == CounterCount); } } SyncStopwatch.Stop(); if (bResync || StatsNodes.Num() != PreviousNodeCount) { // Disable sorting if too many items. if (StatsNodes.Num() > 10000) { ColumnBeingSorted = NAME_None; ColumnSortMode = GetDefaultColumnSortMode(); UpdateCurrentSortingByColumn(); } UpdateTree(); Aggregator->Cancel(); Aggregator->Start(); // Save selection. TArray SelectedItems; TreeView->GetSelectedItems(SelectedItems); TreeView->RebuildList(); // Restore selection. if (SelectedItems.Num() > 0) { TreeView->ClearSelection(); for (FStatsNodePtr& NodePtr : SelectedItems) { NodePtr = GetCounterNode(NodePtr->GetCounterId()); } SelectedItems.RemoveAll([](const FStatsNodePtr& NodePtr) { return !NodePtr.IsValid(); }); if (SelectedItems.Num() > 0) { TreeView->SetItemSelection(SelectedItems, true); TreeView->RequestScrollIntoView(SelectedItems.Last()); } } } Stopwatch.Stop(); const double TotalTime = Stopwatch.GetAccumulatedTime(); if (TotalTime > 0.01) { const double SyncTime = SyncStopwatch.GetAccumulatedTime(); UE_LOG(LogTimingProfiler, Log, TEXT("[Counters] Tree view rebuilt in %.4fs (sync: %.4fs + update: %.4fs) --> %d counters (%d added)"), TotalTime, SyncTime, TotalTime - SyncTime, StatsNodes.Num(), StatsNodes.Num() - PreviousNodeCount); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::UpdateNode(FStatsNodePtr NodePtr) { bool bAddedToGraphFlag = false; if (!NodePtr->IsGroup()) { TSharedPtr Wnd = FTimingProfilerManager::Get()->GetProfilerWindow(); if (Wnd.IsValid()) { TSharedPtr TimingView = Wnd->GetTimingView(); if (TimingView.IsValid()) { TSharedPtr GraphTrack = TimingView->GetMainTimingGraphTrack(); if (GraphTrack.IsValid()) { TSharedPtr Series = GraphTrack->GetStatsCounterSeries(NodePtr->GetCounterId()); bAddedToGraphFlag = Series.IsValid(); } } } } NodePtr->SetAddedToGraphFlag(bAddedToGraphFlag); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ResetStats() { Aggregator->Cancel(); Aggregator->SetTimeInterval(0.0, 0.0); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::UpdateStats(double StartTime, double EndTime) { Aggregator->Cancel(); Aggregator->SetTimeInterval(StartTime, EndTime); Aggregator->Start(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::FinishAggregation() { for (const FStatsNodePtr& NodePtr : StatsNodes) { NodePtr->ResetAggregatedStats(); } Aggregator->ApplyResultsTo(StatsNodesIdMap); Aggregator->ResetResults(); // Invalidate all tree table rows. for (const FStatsNodePtr& NodePtr : StatsNodes) { TSharedPtr TableRowPtr = TreeView->WidgetFromItem(NodePtr); if (TableRowPtr.IsValid()) { TSharedPtr RowPtr = StaticCastSharedPtr(TableRowPtr); RowPtr->InvalidateContent(); } } UpdateTree(); // grouping + sorting + filtering // Ensure the last selected item is visible. const TArray SelectedNodes = TreeView->GetSelectedItems(); if (SelectedNodes.Num() > 0) { TreeView->RequestScrollIntoView(SelectedNodes.Last()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// FStatsNodePtr SStatsView::GetCounterNode(uint32 CounterId) const { return StatsNodesIdMap.FindRef(CounterId); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::SelectCounterNode(uint32 CounterId) { FStatsNodePtr NodePtr = GetCounterNode(CounterId); if (NodePtr) { TreeView->SetSelection(NodePtr); TreeView->RequestScrollIntoView(NodePtr); } } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedPtr SStatsView::GetTimingViewMainGraphTrack() const { TSharedPtr Wnd = FTimingProfilerManager::Get()->GetProfilerWindow(); TSharedPtr TimingView = Wnd.IsValid() ? Wnd->GetTimingView() : nullptr; return TimingView.IsValid() ? TimingView->GetMainTimingGraphTrack() : nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ToggleGraphSeries(TSharedRef GraphTrack, FStatsNodeRef NodePtr) const { const uint32 CounterId = NodePtr->GetCounterId(); TSharedPtr Series = GraphTrack->GetStatsCounterSeries(CounterId); if (Series.IsValid()) { GraphTrack->RemoveStatsCounterSeries(CounterId); GraphTrack->SetDirtyFlag(); NodePtr->SetAddedToGraphFlag(false); } else { GraphTrack->Show(); Series = GraphTrack->AddStatsCounterSeries(CounterId, NodePtr->GetColor()); GraphTrack->SetDirtyFlag(); NodePtr->SetAddedToGraphFlag(true); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::IsSeriesInTimingViewMainGraph(FStatsNodePtr CounterNode) const { TSharedPtr GraphTrack = GetTimingViewMainGraphTrack(); if (GraphTrack.IsValid()) { const uint32 CounterId = CounterNode->GetCounterId(); TSharedPtr Series = GraphTrack->GetStatsCounterSeries(CounterId); return Series.IsValid(); } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ToggleTimingViewMainGraphEventSeries(FStatsNodePtr CounterNode) const { TSharedPtr GraphTrack = GetTimingViewMainGraphTrack(); if (GraphTrack.IsValid()) { ToggleGraphSeries(GraphTrack.ToSharedRef(), CounterNode.ToSharedRef()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // Generate and Edit Colors //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_GenerateNewColor_CanExecute() const { return AreSelectedCounterNodesValid(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_GenerateNewColor_Execute() const { const TArray SelectedNodes = TreeView->GetSelectedItems(); for (const FStatsNodePtr& SelectedNode : SelectedNodes) { if (SelectedNode->Is()) { FStatsNode& StatsNode = SelectedNode->As(); SetRandomColorToNode(StatsNode); } } }; //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::SetColorToNode(FStatsNode& StatsNode, FLinearColor CustomColor) const { StatsNode.SetColor(CustomColor); const uint32 CounterId = StatsNode.GetCounterId(); const FLinearColor BorderColor(CustomColor.R + 0.4f, CustomColor.G + 0.4f, CustomColor.B + 0.4f, 1.0f); TSharedPtr CounterSeries = GetTimingViewMainGraphTrack()->GetStatsCounterSeries(CounterId); if (CounterSeries.IsValid()) { CounterSeries->SetColor(CustomColor, BorderColor); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::SetRandomColorToNode(FStatsNode& StatsNode) const { uint64 Time = FPlatformTime::Cycles64(); uint32 Hash = (Time & 0xFFFFFFFF) ^ (Time >> 32); Hash = ((Hash << 5) + Hash) * 0xfb23618f; const uint8 H = Hash & 0xFF; const uint8 S = 128 + ((Hash >> 8) & 0x7F); const uint8 V = 128 + ((Hash >> 16) & 0x7F); FLinearColor CustomColor = FLinearColor::MakeFromHSV8(H, S, V); CustomColor.A = 1.0f; StatsNode.SetColor(CustomColor); const uint32 CounterId = StatsNode.GetCounterId(); const FLinearColor BorderColor(CustomColor.R + 0.4f, CustomColor.G + 0.4f, CustomColor.B + 0.4f, 1.0f); TSharedPtr CounterSeries = GetTimingViewMainGraphTrack()->GetStatsCounterSeries(CounterId); if (CounterSeries.IsValid()) { CounterSeries->SetColor(CustomColor, BorderColor); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_EditColor_CanExecute() const { return AreSelectedCounterNodesValid(); }; //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_EditColor_Execute() { EditableColorValue = FLinearColor(0.5f, 0.5f, 0.5f, 1.0f); const TArray SelectedNodes = TreeView->GetSelectedItems(); for (const FStatsNodePtr& SelectedNode : SelectedNodes) { if (SelectedNode->Is()) { FStatsNode& StatsNode = SelectedNode->As(); EditableColorValue = StatsNode.GetColor(); break; } } FColorPickerArgs PickerArgs; { PickerArgs.bUseAlpha = true; PickerArgs.bOnlyRefreshOnMouseUp = false; PickerArgs.bOnlyRefreshOnOk = false; PickerArgs.bExpandAdvancedSection = false; PickerArgs.OnColorCommitted = FOnLinearColorValueChanged::CreateSP(this, &SStatsView::SetEditableColor); PickerArgs.OnColorPickerCancelled = FOnColorPickerCancelled::CreateSP(this, &SStatsView::ColorPickerCancelled); PickerArgs.InitialColor = EditableColorValue; PickerArgs.ParentWidget = SharedThis(this); } OpenColorPicker(PickerArgs); }; //////////////////////////////////////////////////////////////////////////////////////////////////// FLinearColor SStatsView::GetEditableColor() const { return EditableColorValue; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::SetEditableColor(FLinearColor NewColor) { EditableColorValue = NewColor; const TArray SelectedNodes = TreeView->GetSelectedItems(); for (const FStatsNodePtr& SelectedNode : SelectedNodes) { if (SelectedNode->Is()) { FStatsNode& StatsNode = SelectedNode->As(); SetColorToNode(StatsNode, EditableColorValue); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ColorPickerCancelled(FLinearColor OriginalColor) { SetEditableColor(OriginalColor); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_CopySelectedToClipboard_CanExecute() const { const TArray SelectedNodes = TreeView->GetSelectedItems(); return SelectedNodes.Num() > 0; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_CopySelectedToClipboard_Execute() { if (Table->IsValid()) { const ESortMode SortMode = ColumnSortMode == EColumnSortMode::Ascending ? Insights::ESortMode::Ascending : Insights::ESortMode::Descending; UE::Insights::CopyToClipboard(Table.ToSharedRef(), TreeView->GetSelectedItems(), CurrentSorter, SortMode); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_CopySelectedNameToClipboard_CanExecute() const { return TreeView->GetSelectedItems().Num() > 0; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_CopySelectedNameToClipboard_Execute() { if (Table->IsValid()) { const ESortMode SortMode = ColumnSortMode == EColumnSortMode::Ascending ? Insights::ESortMode::Ascending : Insights::ESortMode::Descending; UE::Insights::CopyNameToClipboard(Table.ToSharedRef(), TreeView->GetSelectedItems(), CurrentSorter, SortMode); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_Export_CanExecute() const { const TArray SelectedNodes = TreeView->GetSelectedItems(); return SelectedNodes.Num() > 0; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_Export_Execute() { if (!Table->IsValid()) { return; } TArray SelectedNodes; for (FStatsNodePtr Item : TreeView->GetSelectedItems()) { SelectedNodes.Add(Item); } if (SelectedNodes.Num() == 0) { return; } const FString DialogTitle = LOCTEXT("Export_Title", "Export Aggregated Counter Stats").ToString(); const FString DefaultFile = TEXT("CounterStats.tsv"); FString Filename; if (!OpenSaveTextFileDialog(DialogTitle, DefaultFile, Filename)) { return; } IFileHandle* ExportFileHandle = OpenExportFile(*Filename); if (!ExportFileHandle) { return; } FStopwatch Stopwatch; Stopwatch.Start(); UTF16CHAR BOM = UNICODE_BOM; ExportFileHandle->Write((uint8*)&BOM, sizeof(UTF16CHAR)); TCHAR Separator = TEXT('\t'); if (Filename.EndsWith(TEXT(".csv"))) { Separator = TEXT(','); } constexpr TCHAR LineEnd = TEXT('\n'); constexpr TCHAR QuotationMarkBegin = TEXT('\"'); constexpr TCHAR QuotationMarkEnd = TEXT('\"'); TStringBuilder<1024> StringBuilder; TArray> VisibleColumns; Table->GetVisibleColumns(VisibleColumns); // Write header. { bool bIsFirstColumn = true; for (const TSharedRef& ColumnRef : VisibleColumns) { if (bIsFirstColumn) { bIsFirstColumn = false; } else { StringBuilder.AppendChar(Separator); } FString Value = ColumnRef->GetShortName().ToString().ReplaceCharWithEscapedChar(); int32 CharIndex; if (Value.FindChar(Separator, CharIndex)) { StringBuilder.AppendChar(QuotationMarkBegin); StringBuilder.Append(Value); StringBuilder.AppendChar(QuotationMarkEnd); } else { StringBuilder.Append(Value); } } StringBuilder.AppendChar(LineEnd); ExportFileHandle->Write((const uint8*)StringBuilder.ToString(), StringBuilder.Len() * sizeof(TCHAR)); } if (CurrentSorter.IsValid()) { CurrentSorter->Sort(SelectedNodes, ColumnSortMode == EColumnSortMode::Ascending ? ESortMode::Ascending : ESortMode::Descending); } const int32 NodeCount = SelectedNodes.Num(); for (int32 Index = 0; Index < NodeCount; Index++) { const FBaseTreeNodePtr& Node = SelectedNodes[Index]; StringBuilder.Reset(); bool bIsFirstColumn = true; for (const TSharedRef& ColumnRef : VisibleColumns) { if (bIsFirstColumn) { bIsFirstColumn = false; } else { StringBuilder.AppendChar(Separator); } FString Value = ColumnRef->GetValueAsSerializableString(*Node).ReplaceCharWithEscapedChar(); int32 CharIndex; if (Value.FindChar(Separator, CharIndex)) { StringBuilder.AppendChar(QuotationMarkBegin); StringBuilder.Append(Value); StringBuilder.AppendChar(QuotationMarkEnd); } else { StringBuilder.Append(Value); } } StringBuilder.AppendChar(LineEnd); ExportFileHandle->Write((const uint8*)StringBuilder.ToString(), StringBuilder.Len() * sizeof(TCHAR)); } ExportFileHandle->Flush(); delete ExportFileHandle; ExportFileHandle = nullptr; Stopwatch.Stop(); const double TotalTime = Stopwatch.GetAccumulatedTime(); UE_LOG(TraceInsights, Log, TEXT("Exported aggregated counter stats to file in %.3fs (\"%s\")."), TotalTime, *Filename); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_ExportValues_CanExecute() const { const TArray SelectedNodes = TreeView->GetSelectedItems(); return (SelectedNodes.Num() == 1) && !SelectedNodes[0]->IsGroup(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_ExportValues_Execute() const { if (!Session.IsValid()) { return; } const TArray SelectedNodes = TreeView->GetSelectedItems(); if (SelectedNodes.Num() != 1 || SelectedNodes[0]->IsGroup()) { return; } const uint32 CounterId = SelectedNodes[0]->GetCounterId(); FString CounterName = SelectedNodes[0]->GetName().ToString(); const FString DialogTitle = LOCTEXT("ExportValues_Title", "Export Counter Values").ToString(); FString DefaultFile = CounterName.Replace(TEXT("(1/frame)"), TEXT("(1 per frame)")); DefaultFile.ReplaceCharInline(TEXT('.'), TEXT('_')); DefaultFile = FPaths::MakeValidFileName(DefaultFile, TCHAR('_')); if (DefaultFile.IsEmpty() || DefaultFile[DefaultFile.Len() - 1] == TEXT(' ')) { DefaultFile += TEXT('_'); } DefaultFile = TEXT("CounterValues ") + DefaultFile + TEXT(".tsv"); FString Filename; if (!OpenSaveTextFileDialog(DialogTitle, DefaultFile, Filename)) { return; } FTimingExporter Exporter(*Session.Get()); FTimingExporter::FExportCounterParams Params; // default //////////////////////////////////////////////////////////////////////////////////////////////////// // Limit the time interval for enumeration (if a time range selection is made in Timing view). Params.IntervalStartTime = -std::numeric_limits::infinity(); Params.IntervalEndTime = +std::numeric_limits::infinity(); TSharedPtr Wnd = FTimingProfilerManager::Get()->GetProfilerWindow(); TSharedPtr TimingView = Wnd.IsValid() ? Wnd->GetTimingView() : nullptr; if (TimingView.IsValid()) { const double SelectionStartTime = TimingView->GetSelectionStartTime(); const double SelectionEndTime = TimingView->GetSelectionEndTime(); if (SelectionStartTime < SelectionEndTime) { Params.IntervalStartTime = SelectionStartTime; Params.IntervalEndTime = SelectionEndTime; } } //////////////////////////////////////////////////////////////////////////////////////////////////// Exporter.ExportCounterAsText(*Filename, CounterId, Params); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_ExportOps_CanExecute() const { const TArray SelectedNodes = TreeView->GetSelectedItems(); return (SelectedNodes.Num() == 1) && !SelectedNodes[0]->IsGroup(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_ExportOps_Execute() const { if (!Session.IsValid()) { return; } const TArray SelectedNodes = TreeView->GetSelectedItems(); if (SelectedNodes.Num() != 1 || SelectedNodes[0]->IsGroup()) { return; } const uint32 CounterId = SelectedNodes[0]->GetCounterId(); FString CounterName = SelectedNodes[0]->GetName().ToString(); const FString DialogTitle = LOCTEXT("ExportOps_Title", "Export Counter Ops").ToString(); FString DefaultFile = CounterName.Replace(TEXT("(1/frame)"), TEXT("(1 per frame)")); DefaultFile.ReplaceCharInline(TEXT('.'), TEXT('_')); DefaultFile = FPaths::MakeValidFileName(DefaultFile, TCHAR('_')); if (DefaultFile.IsEmpty() || DefaultFile[DefaultFile.Len() - 1] == TEXT(' ')) { DefaultFile += TEXT('_'); } DefaultFile = TEXT("CounterOps ") + DefaultFile + TEXT(".tsv"); FString Filename; if (!OpenSaveTextFileDialog(DialogTitle, DefaultFile, Filename)) { return; } FTimingExporter Exporter(*Session.Get()); FTimingExporter::FExportCounterParams Params; // default Params.bExportOps = true; // export "operations" instead of "values" //////////////////////////////////////////////////////////////////////////////////////////////////// // Limit the time interval for enumeration (if a time range selection is made in Timing view). Params.IntervalStartTime = -std::numeric_limits::infinity(); Params.IntervalEndTime = +std::numeric_limits::infinity(); TSharedPtr Wnd = FTimingProfilerManager::Get()->GetProfilerWindow(); TSharedPtr TimingView = Wnd.IsValid() ? Wnd->GetTimingView() : nullptr; if (TimingView.IsValid()) { const double SelectionStartTime = TimingView->GetSelectionStartTime(); const double SelectionEndTime = TimingView->GetSelectionEndTime(); if (SelectionStartTime < SelectionEndTime) { Params.IntervalStartTime = SelectionStartTime; Params.IntervalEndTime = SelectionEndTime; } } //////////////////////////////////////////////////////////////////////////////////////////////////// Exporter.ExportCounterAsText(*Filename, CounterId, Params); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::ContextMenu_ExportCounters_CanExecute() const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SStatsView::ContextMenu_ExportCounters_Execute() const { if (!Session.IsValid()) { return; } const FString DialogTitle = LOCTEXT("ExportCounters_Title", "Export Counters").ToString(); const FString DefaultFile = TEXT("Counters.tsv"); FString Filename; if (!OpenSaveTextFileDialog(DialogTitle, DefaultFile, Filename)) { return; } FTimingExporter Exporter(*Session.Get()); FTimingExporter::FExportCountersParams Params; // default Exporter.ExportCountersAsText(*Filename, Params); } //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::OpenSaveTextFileDialog(const FString& InDialogTitle, const FString& InDefaultFile, FString& OutFilename) const { TArray SaveFilenames; bool bDialogResult = false; IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); if (DesktopPlatform) { const FString DefaultPath = FPaths::ProjectSavedDir(); bDialogResult = DesktopPlatform->SaveFileDialog( FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr), InDialogTitle, DefaultPath, InDefaultFile, TEXT("Tab-Separated Values (*.tsv)|*.tsv|Text Files (*.txt)|*.txt|Comma-Separated Values (*.csv)|*.csv|All Files (*.*)|*.*"), EFileDialogFlags::None, SaveFilenames ); } if (!bDialogResult || SaveFilenames.Num() == 0) { return false; } OutFilename = SaveFilenames[0]; return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// IFileHandle* SStatsView::OpenExportFile(const TCHAR* InFilename) const { IFileHandle* ExportFileHandle = FPlatformFileManager::Get().GetPlatformFile().OpenWrite(InFilename); if (ExportFileHandle == nullptr) { FName LogListingName = FTimingProfilerManager::Get()->GetLogListingName(); FMessageLog ReportMessageLog((LogListingName != NAME_None) ? LogListingName : TEXT("Other")); ReportMessageLog.Error(LOCTEXT("FailedToOpenFile", "Export failed. Failed to open file for write.")); ReportMessageLog.Notify(); return nullptr; } return ExportFileHandle; } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SStatsView::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { return CommandList->ProcessCommandBindings(InKeyEvent) == true ? FReply::Handled() : FReply::Unhandled(); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SStatsView::AreSelectedCounterNodesValid() const { const TArray SelectedNodes = TreeView->GetSelectedItems(); for (const FStatsNodePtr& SelectedNode : SelectedNodes) { if (!SelectedNode.IsValid()) { return false; } } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// } // namespace UE::Insights::TimingProfiler #undef LOCTEXT_NAMESPACE