// Copyright Epic Games, Inc. All Rights Reserved. #include "STimersView.h" #include "EditorStyleSet.h" #include "Framework/Commands/Commands.h" #include "Framework/Commands/UICommandList.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "HAL/PlatformApplicationMisc.h" #include "SlateOptMacros.h" #include "TraceServices/AnalysisService.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Input/SSearchBox.h" #include "Widgets/Images/SImage.h" #include "Widgets/Layout/SGridPanel.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/SToolTip.h" #include "Widgets/Views/STableViewBase.h" // Insights #include "Insights/Common/Stopwatch.h" #include "Insights/Common/TimeUtils.h" #include "Insights/Table/ViewModels/Table.h" #include "Insights/Table/ViewModels/TableColumn.h" #include "Insights/TimingProfilerCommon.h" #include "Insights/TimingProfilerManager.h" #include "Insights/ViewModels/ThreadTimingTrack.h" #include "Insights/ViewModels/TimerAggregation.h" #include "Insights/ViewModels/TimerNodeHelper.h" #include "Insights/ViewModels/TimersViewColumnFactory.h" #include "Insights/ViewModels/TimingGraphTrack.h" #include "Insights/Widgets/SAggregatorStatus.h" #include "Insights/Widgets/STimersViewTooltip.h" #include "Insights/Widgets/STimerTableRow.h" #include "Insights/Widgets/STimingProfilerWindow.h" #include "Insights/Widgets/STimingView.h" #include #include "DesktopPlatformModule.h" #include "IDesktopPlatform.h" #include "Insights/ViewModels/MinimalTimerExporter.h" #define LOCTEXT_NAMESPACE "STimersView" //////////////////////////////////////////////////////////////////////////////////////////////////// STimersView::STimersView() : Table(MakeShared()), bExpansionSaved(false), bFilterOutZeroCountTimers(false), GroupingMode(ETimerGroupingMode::ByType), AvailableSorters(), CurrentSorter(nullptr), ColumnBeingSorted(GetDefaultColumnBeingSorted()), ColumnSortMode(GetDefaultColumnSortMode()), Aggregator(MakeShared()) { FMemory::Memset(bTimerTypeIsVisible, 1); } //////////////////////////////////////////////////////////////////////////////////////////////////// class FTimersViewCommands: public TCommands { public: FTimersViewCommands() : TCommands(TEXT("STimersViewCommands"), NSLOCTEXT("STimersViewCommands", "Timer View Commands", "Timer View Commands"), NAME_None, FEditorStyle::Get().GetStyleSetName()) { } virtual ~FTimersViewCommands() { } // UI_COMMAND takes long for the compiler to optimize PRAGMA_DISABLE_OPTIMIZATION virtual void RegisterCommands() override { UI_COMMAND(Command_CopyToClipboard, "Copy To Clipboard", "Copies selection to clipboard", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control, EKeys::C)); } PRAGMA_ENABLE_OPTIMIZATION TSharedPtr Command_CopyToClipboard; }; void STimersView::InitCommandList() { FTimersViewCommands::Register(); CommandList = MakeShared(); CommandList->MapAction(FTimersViewCommands::Get().Command_CopyToClipboard, FExecuteAction::CreateSP(this, &STimersView::ContextMenu_CopySelectedToClipboard_Execute), FCanExecuteAction::CreateSP(this, &STimersView::ContextMenu_CopySelectedToClipboard_CanExecute)); } //////////////////////////////////////////////////////////////////////////////////////////////////// STimersView::~STimersView() { // Remove ourselves from the Insights manager. if (FInsightsManager::Get().IsValid()) { FInsightsManager::Get()->GetSessionChangedEvent().RemoveAll(this); } FTimersViewCommands::Unregister(); } //////////////////////////////////////////////////////////////////////////////////////////////////// BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void STimersView::Construct(const FArguments& InArgs) { SAssignNew(ExternalScrollbar, SScrollBar) .AlwaysShowScrollbar(true); ChildSlot [SNew(SVerticalBox) + SVerticalBox::Slot() .VAlign(VAlign_Center) .AutoHeight() [SNew(SBorder) .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(2.0f) [SNew(SVerticalBox) + SVerticalBox::Slot() .VAlign(VAlign_Center) .Padding(2.0f) .AutoHeight() [SNew(SHorizontalBox) // Search box + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(2.0f) .FillWidth(1.0f) [SAssignNew(SearchBox, SSearchBox) .HintText(LOCTEXT("SearchBoxHint", "Search timers or groups")) .OnTextChanged(this, &STimersView::SearchBox_OnTextChanged) .IsEnabled(this, &STimersView::SearchBox_IsEnabled) .ToolTipText(LOCTEXT("FilterSearchHint", "Type here to search timer or group"))] // Filter out timers with zero instance count + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(2.0f) .AutoWidth() [SNew(SCheckBox) .Style(FEditorStyle::Get(), "ToggleButtonCheckbox") .HAlign(HAlign_Center) .Padding(2.0f) .OnCheckStateChanged(this, &STimersView::FilterOutZeroCountTimers_OnCheckStateChanged) .IsChecked(this, &STimersView::FilterOutZeroCountTimers_IsChecked) .ToolTipText(LOCTEXT("FilterOutZeroCountTimers_Tooltip", "Filter out the timers having zero total instance count (aggregated stats).")) [ // TODO: SNew(SImage) SNew(STextBlock) .Text(LOCTEXT("FilterOutZeroCountTimers_Button", " !0 ")) .TextStyle(FEditorStyle::Get(), TEXT("Profiler.Caption"))]]] // Group by + SVerticalBox::Slot() .VAlign(VAlign_Center) .Padding(2.0f) .AutoHeight() [SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) [SNew(STextBlock) .Text(LOCTEXT("GroupByText", "Group by"))] + SHorizontalBox::Slot() .FillWidth(2.0f) .VAlign(VAlign_Center) [SAssignNew(GroupByComboBox, SComboBox>) .ToolTipText(this, &STimersView::GroupBy_GetSelectedTooltipText) .OptionsSource(&GroupByOptionsSource) .OnSelectionChanged(this, &STimersView::GroupBy_OnSelectionChanged) .OnGenerateWidget(this, &STimersView::GroupBy_OnGenerateWidget) [SNew(STextBlock) .Text(this, &STimersView::GroupBy_GetSelectedText)]]] // Check boxes for: GpuScope, ComputeScope, CpuScope + SVerticalBox::Slot() .VAlign(VAlign_Center) .Padding(2.0f) .AutoHeight() [SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(0.0f, 0.0f, 1.0f, 0.0f)) .FillWidth(1.0f) [GetToggleButtonForTimerType(ETimerNodeType::GpuScope)] //+ SHorizontalBox::Slot() //.Padding(FMargin(1.0f,0.0f,1.0f,0.0f)) //.FillWidth(1.0f) //[ // GetToggleButtonForTimerType(ETimerNodeType::ComputeScope) //] + SHorizontalBox::Slot() .Padding(FMargin(1.0f, 0.0f, 1.0f, 0.0f)) .FillWidth(1.0f) [GetToggleButtonForTimerType(ETimerNodeType::CpuScope)]]]] // Tree view + SVerticalBox::Slot() .FillHeight(1.0f) .Padding(0.0f, 6.0f, 0.0f, 0.0f) [SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(0.0f) [SNew(SScrollBox) .Orientation(Orient_Horizontal) + SScrollBox::Slot() [SNew(SOverlay) + SOverlay::Slot() .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) [ // SNew(SBorder) //.BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) //.Padding(0.0f) //[ SAssignNew(TreeView, STreeView) .ExternalScrollbar(ExternalScrollbar) .SelectionMode(ESelectionMode::Multi) .TreeItemsSource(&FilteredGroupNodes) .OnGetChildren(this, &STimersView::TreeView_OnGetChildren) .OnGenerateRow(this, &STimersView::TreeView_OnGenerateRow) .OnSelectionChanged(this, &STimersView::TreeView_OnSelectionChanged) .OnMouseButtonDoubleClick(this, &STimersView::TreeView_OnMouseButtonDoubleClick) .OnContextMenuOpening(FOnContextMenuOpening::CreateSP(this, &STimersView::TreeView_GetMenuContent)) .ItemHeight(12.0f) .HeaderRow( SAssignNew(TreeViewHeaderRow, SHeaderRow) .Visibility(EVisibility::Visible)) //] ] + SOverlay::Slot() .HAlign(HAlign_Right) .VAlign(VAlign_Bottom) .Padding(16.0f) [SAssignNew(AggregatorStatus, Insights::SAggregatorStatus, Aggregator)]]] + SHorizontalBox::Slot() .AutoWidth() .Padding(0.0f) [SNew(SBox) .WidthOverride(FOptionalSize(13.0f)) [ExternalScrollbar.ToSharedRef()]]]]; InitializeAndShowHeaderColumns(); // BindCommands(); // Create the search filters: text based, type based etc. TextFilter = MakeShared(FTimerNodeTextFilter::FItemToStringArray::CreateSP(this, &STimersView::HandleItemToStringArray)); Filters = MakeShared(); Filters->Add(TextFilter); CreateGroupByOptionsSources(); CreateSortings(); InitCommandList(); // Register ourselves with the Insights manager. FInsightsManager::Get()->GetSessionChangedEvent().AddSP(this, &STimersView::InsightsManager_OnSessionChanged); // Update the Session (i.e. when analysis session was already started). InsightsManager_OnSessionChanged(); } END_SLATE_FUNCTION_BUILD_OPTIMIZATION //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedPtr STimersView::TreeView_GetMenuContent() { const TArray SelectedNodes = TreeView->GetSelectedItems(); const int32 NumSelectedNodes = SelectedNodes.Num(); FTimerNodePtr SelectedNode = NumSelectedNodes ? SelectedNodes[0] : nullptr; const TSharedPtr HoveredColumnPtr = Table->FindColumn(HoveredColumnId); FText SelectionStr; FText PropertyName; FText PropertyValue; if (NumSelectedNodes == 0) { SelectionStr = LOCTEXT("NothingSelected", "Nothing selected"); } else if (NumSelectedNodes == 1) { if (HoveredColumnPtr != nullptr) { PropertyName = HoveredColumnPtr->GetShortName(); PropertyValue = HoveredColumnPtr->GetValueAsTooltipText(*SelectedNode); } FString ItemName = SelectedNode->GetName().ToString(); const int32 MaxStringLen = 64; if (ItemName.Len() > MaxStringLen) { ItemName = ItemName.Left(MaxStringLen) + TEXT("..."); } SelectionStr = FText::FromString(ItemName); } else { SelectionStr = LOCTEXT("MultipleSelection", "Multiple selection"); } const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, CommandList.ToSharedRef()); // Selection menu MenuBuilder.BeginSection("Selection", LOCTEXT("ContextMenu_Header_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(FEditorStyle::GetStyleSetName(), "@missing.icon"), DummyUIAction, NAME_None, EUserInterfaceActionType::Button); } MenuBuilder.EndSection(); // Timer options section MenuBuilder.BeginSection("TimerOptions", LOCTEXT("ContextMenu_Header_TimerOptions", "Timer 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->IsGroup(); }; /*Highlight event*/ { FUIAction Action_ToggleHighlight; Action_ToggleHighlight.CanExecuteAction = FCanExecuteAction::CreateLambda(CanExecute); Action_ToggleHighlight.ExecuteAction = FExecuteAction::CreateSP(this, &STimersView::ToggleTimingViewEventFilter, SelectedNode); TSharedPtr Wnd = FTimingProfilerManager::Get()->GetProfilerWindow(); TSharedPtr TimingView = Wnd.IsValid() ? Wnd->GetTimingView() : nullptr; if (SelectedNode.IsValid() && !SelectedNode->IsGroup() && TimingView.IsValid() && TimingView->IsFilterByEventType(SelectedNode->GetTimerId())) { MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Header_TimerOptions_StopHighlightEvent", "Stop Highlighting Event"), LOCTEXT("ContextMenu_Header_TimerOptions_StopHighlightEvent_Desc", "Stops highlighting timing event instances of this timer."), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.EventGraph.FilteredEvent"), Action_ToggleHighlight, NAME_None, EUserInterfaceActionType::Button); } else { MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Header_TimerOptions_HighlightEvent", "Highlight Event"), LOCTEXT("ContextMenu_Header_TimerOptions_HighlightEvent_Desc", "Highlights all timing event instances of this timer."), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.EventGraph.FilteredEvent"), Action_ToggleHighlight, NAME_None, EUserInterfaceActionType::Button); } } /*Add series to timing view graph track*/ { FUIAction Action_ToggleTimerInGraphTrack; Action_ToggleTimerInGraphTrack.CanExecuteAction = FCanExecuteAction::CreateLambda(CanExecute); Action_ToggleTimerInGraphTrack.ExecuteAction = FExecuteAction::CreateSP(this, &STimersView::ToggleTimingViewMainGraphEventSeries, SelectedNode); if (SelectedNode.IsValid() && !SelectedNode->IsGroup() && IsSeriesInTimingViewMainGraph(SelectedNode)) { MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Header_TimerOptions_RemoveFromGraphTrack", "Remove series from graph track"), LOCTEXT("ContextMenu_Header_TimerOptions_RemoveFromGraphTrack_Desc", "Remove the series containing event instances of this timer from the timing graph track."), FSlateIcon(FEditorStyle::GetStyleSetName(), "ProfilerCommand.ToggleShowDataGraph"), Action_ToggleTimerInGraphTrack, NAME_None, EUserInterfaceActionType::Button); } else { MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Header_TimerOptions_AddToGraphTrack", "Add series to graph track"), LOCTEXT("ContextMenu_Header_TimerOptions_AddToGraphTrack_Desc", "Add a series containing event instances of this timer to the timing graph track."), FSlateIcon(FEditorStyle::GetStyleSetName(), "ProfilerCommand.ToggleShowDataGraph"), Action_ToggleTimerInGraphTrack, NAME_None, EUserInterfaceActionType::Button); } } } MenuBuilder.EndSection(); MenuBuilder.BeginSection("Misc", LOCTEXT("ContextMenu_Header_Misc", "Miscellaneous")); { MenuBuilder.AddMenuEntry( FTimersViewCommands::Get().Command_CopyToClipboard, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.Misc.CopyToClipboard")); MenuBuilder.AddSubMenu( LOCTEXT("ContextMenu_Header_Misc_Sort", "Sort By"), LOCTEXT("ContextMenu_Header_Misc_Sort_Desc", "Sort by column"), FNewMenuDelegate::CreateSP(this, &STimersView::TreeView_BuildSortByMenu), false, FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.Misc.SortBy")); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("Columns", LOCTEXT("ContextMenu_Header_Columns", "Columns")); { MenuBuilder.AddSubMenu( LOCTEXT("ContextMenu_Header_Columns_View", "View Column"), LOCTEXT("ContextMenu_Header_Columns_View_Desc", "Hides or shows columns."), FNewMenuDelegate::CreateSP(this, &STimersView::TreeView_BuildViewColumnMenu), false, FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.EventGraph.ViewColumn")); FUIAction Action_ShowAllColumns( FExecuteAction::CreateSP(this, &STimersView::ContextMenu_ShowAllColumns_Execute), FCanExecuteAction::CreateSP(this, &STimersView::ContextMenu_ShowAllColumns_CanExecute)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Header_Columns_ShowAllColumns", "Show All Columns"), LOCTEXT("ContextMenu_Header_Columns_ShowAllColumns_Desc", "Resets tree view to show all columns."), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.EventGraph.ResetColumn"), Action_ShowAllColumns, NAME_None, EUserInterfaceActionType::Button); FUIAction Action_ShowMinMaxMedColumns( FExecuteAction::CreateSP(this, &STimersView::ContextMenu_ShowMinMaxMedColumns_Execute), FCanExecuteAction::CreateSP(this, &STimersView::ContextMenu_ShowMinMaxMedColumns_CanExecute)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Header_Columns_ShowMinMaxMedColumns", "Reset Columns to Min/Max/Median Preset"), LOCTEXT("ContextMenu_Header_Columns_ShowMinMaxMedColumns_Desc", "Resets columns to Min/Max/Median preset."), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.EventGraph.ResetColumn"), Action_ShowMinMaxMedColumns, NAME_None, EUserInterfaceActionType::Button); FUIAction Action_ResetColumns( FExecuteAction::CreateSP(this, &STimersView::ContextMenu_ResetColumns_Execute), FCanExecuteAction::CreateSP(this, &STimersView::ContextMenu_ResetColumns_CanExecute)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Header_Columns_ResetColumns", "Reset Columns to Default"), LOCTEXT("ContextMenu_Header_Columns_ResetColumns_Desc", "Resets columns to default."), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.EventGraph.ResetColumn"), Action_ResetColumns, NAME_None, EUserInterfaceActionType::Button); FUIAction Action_ExportMetadata( FExecuteAction::CreateSP(this, &STimersView::ContextMenu_ExportMetadata_Execute), FCanExecuteAction::CreateSP(this, &STimersView::ContextMenu_ExportMetadata_CanExecute)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_ExportMetadata", "Export Metadata to CSV"), LOCTEXT("ContextMenu_ExportMetadata_Desc", "Export Metadata to a .csv file."), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.EventGraph.ResetColumn"), Action_ExportMetadata, NAME_None, EUserInterfaceActionType::Button); FUIAction Action_ExportTimers( FExecuteAction::CreateSP(this, &STimersView::ContextMenu_ExportTimers_Execute), FCanExecuteAction::CreateSP(this, &STimersView::ContextMenu_ExportTimers_CanExecute)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_ExportTimers", "Export Timers to CSV"), LOCTEXT("ContextMenu_ExportTimers_Desc", "Export all timer definitions to a .csv file."), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.EventGraph.ResetColumn"), Action_ExportTimers, NAME_None, EUserInterfaceActionType::Button); FUIAction Action_ExportTimingEventsAll( FExecuteAction::CreateSP(this, &STimersView::ContextMenu_ExportTimingEvents_Execute), FCanExecuteAction::CreateSP(this, &STimersView::ContextMenu_ExportTimingEvents_CanExecute)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_ExportTimingEventsAll", "Export TimingEventsAll to CSV"), LOCTEXT("ContextMenu_ExportTimingEventsAll_Desc", "Export all events to a CSV file."), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.EventGraph.ResetColumn"), Action_ExportTimingEventsAll, NAME_None, EUserInterfaceActionType::Button); FUIAction Action_ExportHierarchyJSON( FExecuteAction::CreateSP(this, &STimersView::ContextMenu_ExportHierarchyJSON_Execute), FCanExecuteAction::CreateSP(this, &STimersView::ContextMenu_ExportHierarchyJSON_CanExecute)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_ExportHierarchyJSON", "Export HierarchyJSON to CSV"), LOCTEXT("ContextMenu_ExportHierarchyJSON_Desc", "Export all events to a JSON file."), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.EventGraph.ResetColumn"), Action_ExportHierarchyJSON, NAME_None, EUserInterfaceActionType::Button); } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::TreeView_BuildSortByMenu(FMenuBuilder& MenuBuilder) { // TODO: Refactor later @see TSharedPtr SCascadePreviewViewportToolBar::GenerateViewMenu() const MenuBuilder.BeginSection("ColumnName", LOCTEXT("ContextMenu_Header_Misc_ColumnName", "Column Name")); for (const TSharedRef& ColumnRef: Table->GetColumns()) { const Insights::FTableColumn& Column = *ColumnRef; if (Column.IsVisible() && Column.CanBeSorted()) { FUIAction Action_SortByColumn( FExecuteAction::CreateSP(this, &STimersView::ContextMenu_SortByColumn_Execute, Column.GetId()), FCanExecuteAction::CreateSP(this, &STimersView::ContextMenu_SortByColumn_CanExecute, Column.GetId()), FIsActionChecked::CreateSP(this, &STimersView::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_Header_Misc_Sort_SortMode", "Sort Mode")); { FUIAction Action_SortAscending( FExecuteAction::CreateSP(this, &STimersView::ContextMenu_SortMode_Execute, EColumnSortMode::Ascending), FCanExecuteAction::CreateSP(this, &STimersView::ContextMenu_SortMode_CanExecute, EColumnSortMode::Ascending), FIsActionChecked::CreateSP(this, &STimersView::ContextMenu_SortMode_IsChecked, EColumnSortMode::Ascending)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Header_Misc_Sort_SortAscending", "Sort Ascending"), LOCTEXT("ContextMenu_Header_Misc_Sort_SortAscending_Desc", "Sorts ascending"), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.Misc.SortAscending"), Action_SortAscending, NAME_None, EUserInterfaceActionType::RadioButton); FUIAction Action_SortDescending( FExecuteAction::CreateSP(this, &STimersView::ContextMenu_SortMode_Execute, EColumnSortMode::Descending), FCanExecuteAction::CreateSP(this, &STimersView::ContextMenu_SortMode_CanExecute, EColumnSortMode::Descending), FIsActionChecked::CreateSP(this, &STimersView::ContextMenu_SortMode_IsChecked, EColumnSortMode::Descending)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Header_Misc_Sort_SortDescending", "Sort Descending"), LOCTEXT("ContextMenu_Header_Misc_Sort_SortDescending_Desc", "Sorts descending"), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.Misc.SortDescending"), Action_SortDescending, NAME_None, EUserInterfaceActionType::RadioButton); } MenuBuilder.EndSection(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::TreeView_BuildViewColumnMenu(FMenuBuilder& MenuBuilder) { MenuBuilder.BeginSection("ViewColumn", LOCTEXT("ContextMenu_Header_Columns_View", "View Column")); for (const TSharedRef& ColumnRef: Table->GetColumns()) { const Insights::FTableColumn& Column = *ColumnRef; FUIAction Action_ToggleColumn( FExecuteAction::CreateSP(this, &STimersView::ToggleColumnVisibility, Column.GetId()), FCanExecuteAction::CreateSP(this, &STimersView::CanToggleColumnVisibility, Column.GetId()), FIsActionChecked::CreateSP(this, &STimersView::IsColumnVisible, Column.GetId())); MenuBuilder.AddMenuEntry( Column.GetTitleName(), Column.GetDescription(), FSlateIcon(), Action_ToggleColumn, NAME_None, EUserInterfaceActionType::ToggleButton); } MenuBuilder.EndSection(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::InitializeAndShowHeaderColumns() { // Create columns. TArray> Columns; FTimersViewColumnFactory::CreateTimersViewColumns(Columns); Table->SetColumns(Columns); // Show columns. for (const TSharedRef& ColumnRef: Table->GetColumns()) { if (ColumnRef->ShouldBeVisible()) { ShowColumn(ColumnRef->GetId()); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// FText STimersView::GetColumnHeaderText(const FName ColumnId) const { const Insights::FTableColumn& Column = *Table->FindColumnChecked(ColumnId); return Column.GetShortName(); } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedRef STimersView::TreeViewHeaderRow_GenerateColumnMenu(const Insights::FTableColumn& Column) { bool bIsMenuVisible = false; const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, NULL); { if (Column.CanBeHidden()) { MenuBuilder.BeginSection("Column", LOCTEXT("TreeViewHeaderRow_Header_Column", "Column")); FUIAction Action_HideColumn( FExecuteAction::CreateSP(this, &STimersView::HideColumn, Column.GetId()), FCanExecuteAction::CreateSP(this, &STimersView::CanHideColumn, Column.GetId())); MenuBuilder.AddMenuEntry( LOCTEXT("TreeViewHeaderRow_HideColumn", "Hide"), LOCTEXT("TreeViewHeaderRow_HideColumn_Desc", "Hides the selected column"), FSlateIcon(), Action_HideColumn, NAME_None, EUserInterfaceActionType::Button); bIsMenuVisible = true; MenuBuilder.EndSection(); } if (Column.CanBeSorted()) { MenuBuilder.BeginSection("SortMode", LOCTEXT("ContextMenu_Header_Misc_Sort_SortMode", "Sort Mode")); FUIAction Action_SortAscending( FExecuteAction::CreateSP(this, &STimersView::HeaderMenu_SortMode_Execute, Column.GetId(), EColumnSortMode::Ascending), FCanExecuteAction::CreateSP(this, &STimersView::HeaderMenu_SortMode_CanExecute, Column.GetId(), EColumnSortMode::Ascending), FIsActionChecked::CreateSP(this, &STimersView::HeaderMenu_SortMode_IsChecked, Column.GetId(), EColumnSortMode::Ascending)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Header_Misc_Sort_SortAscending", "Sort Ascending"), LOCTEXT("ContextMenu_Header_Misc_Sort_SortAscending_Desc", "Sorts ascending"), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.Misc.SortAscending"), Action_SortAscending, NAME_None, EUserInterfaceActionType::RadioButton); FUIAction Action_SortDescending( FExecuteAction::CreateSP(this, &STimersView::HeaderMenu_SortMode_Execute, Column.GetId(), EColumnSortMode::Descending), FCanExecuteAction::CreateSP(this, &STimersView::HeaderMenu_SortMode_CanExecute, Column.GetId(), EColumnSortMode::Descending), FIsActionChecked::CreateSP(this, &STimersView::HeaderMenu_SortMode_IsChecked, Column.GetId(), EColumnSortMode::Descending)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Header_Misc_Sort_SortDescending", "Sort Descending"), LOCTEXT("ContextMenu_Header_Misc_Sort_SortDescending_Desc", "Sorts descending"), FSlateIcon(FEditorStyle::GetStyleSetName(), "Profiler.Misc.SortDescending"), Action_SortDescending, NAME_None, EUserInterfaceActionType::RadioButton); bIsMenuVisible = true; MenuBuilder.EndSection(); } // if (Column.CanBeFiltered()) //{ // MenuBuilder.BeginSection("FilterMode", LOCTEXT("ContextMenu_Header_Misc_Filter_FilterMode", "Filter Mode")); // bIsMenuVisible = true; // MenuBuilder.EndSection(); // } } /* TODO: - Show top ten - Show top bottom - Filter by list (avg, median, 10%, 90%, etc.) - Text box for filtering for each column instead of one text box used for filtering - Grouping button for flat view modes (show at most X groups, show all groups for names) */ return bIsMenuVisible ? MenuBuilder.MakeWidget() : (TSharedRef)SNullWidget::NullWidget; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::InsightsManager_OnSessionChanged() { TSharedPtr NewSession = FInsightsManager::Get()->GetSession(); if (NewSession != Session) { Session = NewSession; Reset(); } else { UpdateTree(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::UpdateTree() { FStopwatch Stopwatch; Stopwatch.Start(); CreateGroups(); Stopwatch.Update(); const double Time1 = Stopwatch.GetAccumulatedTime(); SortTreeNodes(); Stopwatch.Update(); const double Time2 = Stopwatch.GetAccumulatedTime(); ApplyFiltering(); Stopwatch.Stop(); const double TotalTime = Stopwatch.GetAccumulatedTime(); if (TotalTime > 0.1) { UE_LOG(TimingProfiler, Log, TEXT("[Timers] Tree view updated in %.3fs (%d timers) --> G:%.3fs + S:%.3fs + F:%.3fs"), TotalTime, TimerNodes.Num(), Time1, Time2 - Time1, TotalTime - Time2); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ApplyFiltering() { FilteredGroupNodes.Reset(); // Apply filter to all groups and its children. const int32 NumGroups = GroupNodes.Num(); for (int32 GroupIndex = 0; GroupIndex < NumGroups; ++GroupIndex) { FTimerNodePtr& GroupPtr = GroupNodes[GroupIndex]; GroupPtr->ClearFilteredChildren(); const bool bIsGroupVisible = Filters->PassesAllFilters(GroupPtr); const TArray& GroupChildren = GroupPtr->GetChildren(); int32 NumVisibleChildren = 0; for (const Insights::FBaseTreeNodePtr& ChildPtr: GroupChildren) { const FTimerNodePtr& NodePtr = StaticCastSharedPtr(ChildPtr); const bool bIsChildVisible = (!bFilterOutZeroCountTimers || NodePtr->GetAggregatedStats().InstanceCount > 0) && bTimerTypeIsVisible[static_cast(NodePtr->GetType())] && 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 timer 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 FTimerNodePtr& 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 (FTimerNodePtr& GroupPtr: FilteredGroupNodes) { Trace::FTimingProfilerAggregatedStats& AggregatedStats = GroupPtr->GetAggregatedStats(); GroupPtr->ResetAggregatedStats(); constexpr double NanTimeValue = std::numeric_limits::quiet_NaN(); AggregatedStats.AverageInclusiveTime = NanTimeValue; AggregatedStats.MedianInclusiveTime = NanTimeValue; AggregatedStats.AverageExclusiveTime = NanTimeValue; AggregatedStats.MedianExclusiveTime = NanTimeValue; const TArray& GroupChildren = GroupPtr->GetFilteredChildren(); for (const Insights::FBaseTreeNodePtr& ChildPtr: GroupChildren) { const FTimerNodePtr& NodePtr = StaticCastSharedPtr(ChildPtr); Trace::FTimingProfilerAggregatedStats& NodeAggregatedStats = NodePtr->GetAggregatedStats(); if (NodeAggregatedStats.InstanceCount > 0) { AggregatedStats.InstanceCount += NodeAggregatedStats.InstanceCount; AggregatedStats.TotalInclusiveTime += NodeAggregatedStats.TotalInclusiveTime; AggregatedStats.MinInclusiveTime = FMath::Min(AggregatedStats.MinInclusiveTime, NodeAggregatedStats.MinInclusiveTime); AggregatedStats.MaxInclusiveTime = FMath::Max(AggregatedStats.MaxInclusiveTime, NodeAggregatedStats.MaxInclusiveTime); AggregatedStats.TotalExclusiveTime += NodeAggregatedStats.TotalExclusiveTime; AggregatedStats.MinExclusiveTime = FMath::Min(AggregatedStats.MinExclusiveTime, NodeAggregatedStats.MinExclusiveTime); AggregatedStats.MaxExclusiveTime = FMath::Max(AggregatedStats.MaxExclusiveTime, NodeAggregatedStats.MaxExclusiveTime); } } } // Request tree refresh TreeView->RequestTreeRefresh(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::HandleItemToStringArray(const FTimerNodePtr& FTimerNodePtr, TArray& OutSearchStrings) const { OutSearchStrings.Add(FTimerNodePtr->GetName().GetPlainNameString()); } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedRef STimersView::GetToggleButtonForTimerType(const ETimerNodeType NodeType) { return SNew(SCheckBox) .Style(FEditorStyle::Get(), "ToggleButtonCheckbox") .HAlign(HAlign_Center) .Padding(2.0f) .OnCheckStateChanged(this, &STimersView::FilterByTimerType_OnCheckStateChanged, NodeType) .IsChecked(this, &STimersView::FilterByTimerType_IsChecked, NodeType) .ToolTipText(TimerNodeTypeHelper::ToDescription(NodeType)) [SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [SNew(SImage) .Image(TimerNodeTypeHelper::GetIconForTimerNodeType(NodeType))] + SHorizontalBox::Slot() .Padding(2.0f, 0.0f, 0.0f, 0.0f) .VAlign(VAlign_Center) [SNew(STextBlock) .Text(TimerNodeTypeHelper::ToText(NodeType)) .TextStyle(FEditorStyle::Get(), TEXT("Profiler.Caption"))]]; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::FilterOutZeroCountTimers_OnCheckStateChanged(ECheckBoxState NewRadioState) { bFilterOutZeroCountTimers = (NewRadioState == ECheckBoxState::Checked); ApplyFiltering(); } //////////////////////////////////////////////////////////////////////////////////////////////////// ECheckBoxState STimersView::FilterOutZeroCountTimers_IsChecked() const { return bFilterOutZeroCountTimers ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::FilterByTimerType_OnCheckStateChanged(ECheckBoxState NewRadioState, const ETimerNodeType InStatType) { bTimerTypeIsVisible[static_cast(InStatType)] = (NewRadioState == ECheckBoxState::Checked); ApplyFiltering(); } //////////////////////////////////////////////////////////////////////////////////////////////////// ECheckBoxState STimersView::FilterByTimerType_IsChecked(const ETimerNodeType InStatType) const { return bTimerTypeIsVisible[static_cast(InStatType)] ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } //////////////////////////////////////////////////////////////////////////////////////////////////// // TreeView //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::TreeView_Refresh() { if (TreeView.IsValid()) { TreeView->RequestTreeRefresh(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::TreeView_OnSelectionChanged(FTimerNodePtr SelectedItem, ESelectInfo::Type SelectInfo) { if (SelectInfo != ESelectInfo::Direct) { TArray SelectedItems = TreeView->GetSelectedItems(); if (SelectedItems.Num() == 1 && !SelectedItems[0]->IsGroup()) { FTimingProfilerManager::Get()->SetSelectedTimer(SelectedItems[0]->GetTimerId()); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::TreeView_OnGetChildren(FTimerNodePtr InParent, TArray& OutChildren) { const TArray& Children = InParent->GetFilteredChildren(); OutChildren.Reset(Children.Num()); for (const Insights::FBaseTreeNodePtr& Child: Children) { OutChildren.Add(StaticCastSharedPtr(Child)); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::TreeView_OnMouseButtonDoubleClick(FTimerNodePtr NodePtr) { if (NodePtr->IsGroup()) { const bool bIsGroupExpanded = TreeView->IsItemExpanded(NodePtr); TreeView->SetItemExpansion(NodePtr, !bIsGroupExpanded); } else { if (FSlateApplication::Get().GetModifierKeys().IsControlDown()) { ToggleTimingViewEventFilter(NodePtr); } else { ToggleTimingViewMainGraphEventSeries(NodePtr); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// // Tree View's Table Row //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedRef STimersView::TreeView_OnGenerateRow(FTimerNodePtr NodePtr, const TSharedRef& OwnerTable) { TSharedRef TableRow = SNew(STimerTableRow, OwnerTable) .OnShouldBeEnabled(this, &STimersView::TableRow_ShouldBeEnabled) .OnIsColumnVisible(this, &STimersView::IsColumnVisible) .OnSetHoveredCell(this, &STimersView::TableRow_SetHoveredCell) .OnGetColumnOutlineHAlignmentDelegate(this, &STimersView::TableRow_GetColumnOutlineHAlignment) .HighlightText(this, &STimersView::TableRow_GetHighlightText) .HighlightedNodeName(this, &STimersView::TableRow_GetHighlightedNodeName) .TablePtr(Table) .TimerNodePtr(NodePtr); return TableRow; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::TableRow_ShouldBeEnabled(FTimerNodePtr NodePtr) const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::TableRow_SetHoveredCell(TSharedPtr InTablePtr, TSharedPtr InColumnPtr, FTimerNodePtr InNodePtr) { HoveredColumnId = InColumnPtr ? InColumnPtr->GetId() : FName(); const bool bIsAnyMenusVisible = FSlateApplication::Get().AnyMenusVisible(); if (!HasMouseCapture() && !bIsAnyMenusVisible) { HoveredNodePtr = InNodePtr; } } //////////////////////////////////////////////////////////////////////////////////////////////////// EHorizontalAlignment STimersView::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 STimersView::TableRow_GetHighlightText() const { return SearchBox->GetText(); } //////////////////////////////////////////////////////////////////////////////////////////////////// FName STimersView::TableRow_GetHighlightedNodeName() const { return HighlightedNodeName; } //////////////////////////////////////////////////////////////////////////////////////////////////// // SearchBox //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::SearchBox_OnTextChanged(const FText& InFilterText) { TextFilter->SetRawFilterText(InFilterText); SearchBox->SetError(TextFilter->GetFilterErrorText()); ApplyFiltering(); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::SearchBox_IsEnabled() const { return TimerNodes.Num() > 0; } //////////////////////////////////////////////////////////////////////////////////////////////////// // Grouping //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::CreateGroups() { if (GroupingMode == ETimerGroupingMode::Flat) { GroupNodes.Reset(); const FName GroupName(TEXT("All")); FTimerNodePtr GroupPtr = MakeShared(GroupName); GroupNodes.Add(GroupPtr); for (const FTimerNodePtr& NodePtr: TimerNodes) { GroupPtr->AddChildAndSetGroupPtr(NodePtr); } TreeView->SetItemExpansion(GroupPtr, true); } // Creates groups based on stat metadata groups. else if (GroupingMode == ETimerGroupingMode::ByMetaGroupName) { TMap GroupNodeSet; for (const FTimerNodePtr& NodePtr: TimerNodes) { const FName GroupName = NodePtr->GetMetaGroupName(); FTimerNodePtr GroupPtr = GroupNodeSet.FindRef(GroupName); if (!GroupPtr) { GroupPtr = GroupNodeSet.Add(GroupName, MakeShared(GroupName)); } GroupPtr->AddChildAndSetGroupPtr(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 stat type. else if (GroupingMode == ETimerGroupingMode::ByType) { TMap GroupNodeSet; for (const FTimerNodePtr& NodePtr: TimerNodes) { const ETimerNodeType NodeType = NodePtr->GetType(); FTimerNodePtr GroupPtr = GroupNodeSet.FindRef(NodeType); if (!GroupPtr) { const FName GroupName = *TimerNodeTypeHelper::ToText(NodeType).ToString(); GroupPtr = GroupNodeSet.Add(NodeType, MakeShared(GroupName)); } GroupPtr->AddChildAndSetGroupPtr(NodePtr); TreeView->SetItemExpansion(GroupPtr, true); } GroupNodeSet.KeySort([](const ETimerNodeType& A, const ETimerNodeType& B) { return A < B; }); // sort groups by type GroupNodeSet.GenerateValueArray(GroupNodes); } // Creates one group for one letter. else if (GroupingMode == ETimerGroupingMode::ByName) { TMap GroupNodeSet; for (const FTimerNodePtr& NodePtr: TimerNodes) { FString FirstLetterStr(NodePtr->GetName().GetPlainNameString().Left(1).ToUpper()); const TCHAR FirstLetter = FirstLetterStr[0]; FTimerNodePtr GroupPtr = GroupNodeSet.FindRef(FirstLetter); if (!GroupPtr) { const FName GroupName(FirstLetterStr); GroupPtr = GroupNodeSet.Add(FirstLetter, MakeShared(GroupName)); } GroupPtr->AddChildAndSetGroupPtr(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 == ETimerGroupingMode::ByInstanceCount) { 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 FTimerNodePtr& NodePtr: TimerNodes) { uint64 InstanceCount = NodePtr->GetAggregatedStats().InstanceCount; uint32 Order = 0; while (InstanceCount) { InstanceCount /= 10; Order++; } FTimerNodePtr 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->AddChildAndSetGroupPtr(NodePtr); } GroupNodeSet.KeySort([](const uint32& A, const uint32& B) { return A > B; }); // sort groups by order GroupNodeSet.GenerateValueArray(GroupNodes); } // Creates one group for each logarithmic range ie. 0.001 - 0.01, 0.01 - 0.1, 0.1 - 1.0, 1.0 - 10.0, etc. else if (GroupingMode == ETimerGroupingMode::ByTotalInclusiveTime) { // im:TODO: } // Creates one group for each logarithmic range ie. 0.001 - 0.01, 0.01 - 0.1, 0.1 - 1.0, 1.0 - 10.0, etc. else if (GroupingMode == ETimerGroupingMode::ByTotalExclusiveTime) { // im:TODO: } } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::CreateGroupByOptionsSources() { GroupByOptionsSource.Reset(3); // Must be added in order of elements in the ETimerGroupingMode. GroupByOptionsSource.Add(MakeShared(ETimerGroupingMode::Flat)); GroupByOptionsSource.Add(MakeShared(ETimerGroupingMode::ByName)); // GroupByOptionsSource.Add(MakeShared(ETimerGroupingMode::ByMetaGroupName)); GroupByOptionsSource.Add(MakeShared(ETimerGroupingMode::ByType)); GroupByOptionsSource.Add(MakeShared(ETimerGroupingMode::ByInstanceCount)); ETimerGroupingModePtr* GroupingModePtrPtr = GroupByOptionsSource.FindByPredicate([&](const ETimerGroupingModePtr InGroupingModePtr) { return *InGroupingModePtr == GroupingMode; }); if (GroupingModePtrPtr != nullptr) { GroupByComboBox->SetSelectedItem(*GroupingModePtrPtr); } GroupByComboBox->RefreshOptions(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::GroupBy_OnSelectionChanged(TSharedPtr NewGroupingMode, ESelectInfo::Type SelectInfo) { if (SelectInfo != ESelectInfo::Direct) { GroupingMode = *NewGroupingMode; CreateGroups(); SortTreeNodes(); ApplyFiltering(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedRef STimersView::GroupBy_OnGenerateWidget(TSharedPtr InGroupingMode) const { return SNew(STextBlock) .Text(TimerNodeGroupingHelper::ToText(*InGroupingMode)) .ToolTipText(TimerNodeGroupingHelper::ToDescription(*InGroupingMode)); } //////////////////////////////////////////////////////////////////////////////////////////////////// FText STimersView::GroupBy_GetSelectedText() const { return TimerNodeGroupingHelper::ToText(GroupingMode); } //////////////////////////////////////////////////////////////////////////////////////////////////// FText STimersView::GroupBy_GetSelectedTooltipText() const { return TimerNodeGroupingHelper::ToDescription(GroupingMode); } //////////////////////////////////////////////////////////////////////////////////////////////////// // Sorting //////////////////////////////////////////////////////////////////////////////////////////////////// const FName STimersView::GetDefaultColumnBeingSorted() { return FTimersViewColumns::TotalInclusiveTimeColumnID; } //////////////////////////////////////////////////////////////////////////////////////////////////// const EColumnSortMode::Type STimersView::GetDefaultColumnSortMode() { return EColumnSortMode::Descending; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::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 STimersView::UpdateCurrentSortingByColumn() { TSharedPtr ColumnPtr = Table->FindColumn(ColumnBeingSorted); CurrentSorter = ColumnPtr.IsValid() ? ColumnPtr->GetValueSorter() : nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::SortTreeNodes() { if (CurrentSorter.IsValid()) { for (FTimerNodePtr& Root: GroupNodes) { SortTreeNodesRec(*Root, *CurrentSorter); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::SortTreeNodesRec(FTimerNode& Node, const Insights::ITableCellValueSorter& Sorter) { if (ColumnSortMode == EColumnSortMode::Type::Descending) { Node.SortChildrenDescending(Sorter); } else // if (ColumnSortMode == EColumnSortMode::Type::Ascending) { Node.SortChildrenAscending(Sorter); } // for (Insights::FBaseTreeNodePtr ChildPtr : Node.GetChildren()) //{ // //if (ChildPtr->IsGroup()) // if (ChildPtr->GetChildren().Num() > 0) // { // SortTreeNodesRec(*StaticCastSharedPtr(ChildPtr), Sorter); // } // } } //////////////////////////////////////////////////////////////////////////////////////////////////// EColumnSortMode::Type STimersView::GetSortModeForColumn(const FName ColumnId) const { if (ColumnBeingSorted != ColumnId) { return EColumnSortMode::None; } return ColumnSortMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::SetSortModeForColumn(const FName& ColumnId, const EColumnSortMode::Type SortMode) { ColumnBeingSorted = ColumnId; ColumnSortMode = SortMode; UpdateCurrentSortingByColumn(); SortTreeNodes(); ApplyFiltering(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::OnSortModeChanged(const EColumnSortPriority::Type SortPriority, const FName& ColumnId, const EColumnSortMode::Type SortMode) { SetSortModeForColumn(ColumnId, SortMode); TreeView_Refresh(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // SortMode action (HeaderMenu) //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::HeaderMenu_SortMode_IsChecked(const FName ColumnId, const EColumnSortMode::Type InSortMode) { return ColumnBeingSorted == ColumnId && ColumnSortMode == InSortMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::HeaderMenu_SortMode_CanExecute(const FName ColumnId, const EColumnSortMode::Type InSortMode) const { const Insights::FTableColumn& Column = *Table->FindColumnChecked(ColumnId); return Column.CanBeSorted(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::HeaderMenu_SortMode_Execute(const FName ColumnId, const EColumnSortMode::Type InSortMode) { SetSortModeForColumn(ColumnId, InSortMode); TreeView_Refresh(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // SortMode action (ContextMenu) //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::ContextMenu_SortMode_IsChecked(const EColumnSortMode::Type InSortMode) { return ColumnSortMode == InSortMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::ContextMenu_SortMode_CanExecute(const EColumnSortMode::Type InSortMode) const { return true; // ColumnSortMode != InSortMode; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ContextMenu_SortMode_Execute(const EColumnSortMode::Type InSortMode) { SetSortModeForColumn(ColumnBeingSorted, InSortMode); TreeView_Refresh(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // SortByColumn action (ContextMenu) //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::ContextMenu_SortByColumn_IsChecked(const FName ColumnId) { return ColumnId == ColumnBeingSorted; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::ContextMenu_SortByColumn_CanExecute(const FName ColumnId) const { return true; // ColumnId != ColumnBeingSorted; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ContextMenu_SortByColumn_Execute(const FName ColumnId) { SetSortModeForColumn(ColumnId, EColumnSortMode::Descending); TreeView_Refresh(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // ShowColumn action //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::CanShowColumn(const FName ColumnId) const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ShowColumn(const FName ColumnId) { Insights::FTableColumn& Column = *Table->FindColumnChecked(ColumnId); Column.Show(); SHeaderRow::FColumn::FArguments ColumnArgs; ColumnArgs .ColumnId(Column.GetId()) .DefaultLabel(Column.GetShortName()) .HAlignHeader(HAlign_Fill) .VAlignHeader(VAlign_Fill) .HeaderContentPadding(FMargin(2.0f)) .HAlignCell(HAlign_Fill) .VAlignCell(VAlign_Fill) .SortMode(this, &STimersView::GetSortModeForColumn, Column.GetId()) .OnSort(this, &STimersView::OnSortModeChanged) .ManualWidth(Column.GetInitialWidth()) .FixedWidth(Column.IsFixedWidth() ? Column.GetInitialWidth() : TOptional()) .HeaderContent() [SNew(SBox) .ToolTip(STimersViewTooltip::GetColumnTooltip(Column)) .HAlign(Column.GetHorizontalAlignment()) .VAlign(VAlign_Center) [SNew(STextBlock) .Text(this, &STimersView::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 STimersView::CanHideColumn(const FName ColumnId) const { const Insights::FTableColumn& Column = *Table->FindColumnChecked(ColumnId); return Column.CanBeHidden(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::HideColumn(const FName ColumnId) { Insights::FTableColumn& Column = *Table->FindColumnChecked(ColumnId); Column.Hide(); TreeViewHeaderRow->RemoveColumn(ColumnId); } //////////////////////////////////////////////////////////////////////////////////////////////////// // ToggleColumn action //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::IsColumnVisible(const FName ColumnId) const { const Insights::FTableColumn& Column = *Table->FindColumnChecked(ColumnId); return Column.IsVisible(); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::CanToggleColumnVisibility(const FName ColumnId) const { const Insights::FTableColumn& Column = *Table->FindColumnChecked(ColumnId); return !Column.IsVisible() || Column.CanBeHidden(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ToggleColumnVisibility(const FName ColumnId) { const Insights::FTableColumn& Column = *Table->FindColumnChecked(ColumnId); if (Column.IsVisible()) { HideColumn(ColumnId); } else { ShowColumn(ColumnId); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // "Show All Columns" action (ContextMenu) //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::ContextMenu_ShowAllColumns_CanExecute() const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ContextMenu_ShowAllColumns_Execute() { ColumnBeingSorted = GetDefaultColumnBeingSorted(); ColumnSortMode = GetDefaultColumnSortMode(); UpdateCurrentSortingByColumn(); for (const TSharedRef& ColumnRef: Table->GetColumns()) { const Insights::FTableColumn& Column = *ColumnRef; if (!Column.IsVisible()) { ShowColumn(Column.GetId()); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// // "Show Min/Max/Median Columns" action (ContextMenu) //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::ContextMenu_ShowMinMaxMedColumns_CanExecute() const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ContextMenu_ShowMinMaxMedColumns_Execute() { TSet Preset = { FTimersViewColumns::NameColumnID, // FTimersViewColumns::MetaGroupNameColumnID, // FTimersViewColumns::TypeColumnID, FTimersViewColumns::InstanceCountColumnID, FTimersViewColumns::TotalInclusiveTimeColumnID, FTimersViewColumns::MaxInclusiveTimeColumnID, FTimersViewColumns::MedianInclusiveTimeColumnID, FTimersViewColumns::MinInclusiveTimeColumnID, FTimersViewColumns::TotalExclusiveTimeColumnID, FTimersViewColumns::MaxExclusiveTimeColumnID, FTimersViewColumns::MedianExclusiveTimeColumnID, FTimersViewColumns::MinExclusiveTimeColumnID, }; ColumnBeingSorted = FTimersViewColumns::TotalInclusiveTimeColumnID; ColumnSortMode = EColumnSortMode::Descending; UpdateCurrentSortingByColumn(); for (const TSharedRef& ColumnRef: Table->GetColumns()) { const Insights::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 STimersView::ContextMenu_ResetColumns_CanExecute() const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ContextMenu_ResetColumns_Execute() { ColumnBeingSorted = GetDefaultColumnBeingSorted(); ColumnSortMode = GetDefaultColumnSortMode(); UpdateCurrentSortingByColumn(); for (const TSharedRef& ColumnRef: Table->GetColumns()) { const Insights::FTableColumn& Column = *ColumnRef; if (Column.ShouldBeVisible() && !Column.IsVisible()) { ShowColumn(Column.GetId()); } else if (!Column.ShouldBeVisible() && Column.IsVisible()) { HideColumn(Column.GetId()); } } } bool STimersView::ContextMenu_ExportMetadata_CanExecute() const { return true; } void STimersView::ContextMenu_ExportMetadata_Execute() { // 打开保存对话框 FString SaveFilePath; IDesktopPlatform* Desktop = FDesktopPlatformModule::Get(); if (Desktop) { FString FullTracePath = FString(Session->GetName()); FString TraceName = FPaths::GetBaseFilename(FullTracePath); FString DefaultCSV = TraceName + TEXT("_Metadata.csv"); TArray OutFiles; const bool bFileChosen = Desktop->SaveFileDialog( nullptr, // Parent window TEXT("Export Metadata to CSV"), // Dialog Title FPaths::ProjectSavedDir(), // Default Path DefaultCSV, // Default File TEXT("CSV Files (*.csv)|*.csv"), // Filter 0, OutFiles); if (!bFileChosen || OutFiles.Num() == 0) { return; // 用户取消 } SaveFilePath = OutFiles[0]; } if (SaveFilePath.IsEmpty()) { return; } if (!Session) { UE_LOG(LogTemp, Warning, TEXT("No analysis session!")); return; } if (FMinimalTimerExporter::ExportMetadataToCSV(*Session, SaveFilePath, TraceTimer::FExportFilter{TEXT("ContextMenu_ExportMetadata_Execute")})) { UE_LOG(LogTemp, Log, TEXT("Exported Metadata to CSV: %s"), *SaveFilePath); } else { UE_LOG(LogTemp, Error, TEXT("Failed to export Metadata to CSV: %s"), *SaveFilePath); } } bool STimersView::ContextMenu_ExportTimers_CanExecute() const { return true; } void STimersView::ContextMenu_ExportTimers_Execute() { // 打开保存对话框 FString SaveFilePath; IDesktopPlatform* Desktop = FDesktopPlatformModule::Get(); if (Desktop) { FString TraceName = FString(Session->GetName()); FString DefaultCSV = TraceName + TEXT(".csv"); TArray OutFiles; const bool bFileChosen = Desktop->SaveFileDialog( nullptr, // Parent window TEXT("Export Timers to CSV"), // Dialog Title FPaths::ProjectSavedDir(), // Default Path DefaultCSV, // Default File TEXT("CSV Files (*.csv)|*.csv"), // Filter 0, OutFiles); if (!bFileChosen || OutFiles.Num() == 0) { return; // 用户取消 } SaveFilePath = OutFiles[0]; } if (SaveFilePath.IsEmpty()) { return; } if (!Session) { UE_LOG(LogTemp, Warning, TEXT("No analysis session!")); return; } if (FMinimalTimerExporter::ExportTimersToCSV(*Session, SaveFilePath)) { UE_LOG(LogTemp, Log, TEXT("Exported timers to CSV: %s"), *SaveFilePath); } else { UE_LOG(LogTemp, Error, TEXT("Failed to export timers to CSV: %s"), *SaveFilePath); } } bool STimersView::ContextMenu_ExportTimingEvents_CanExecute() const { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::Reset() { Aggregator->Cancel(); Aggregator->SetTimeInterval(0.0, 0.0); RebuildTree(true); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { // Check if we need to update the lists of timers, but not too often. static uint64 NextTimestamp = 0; const uint64 Time = FPlatformTime::Cycles64(); if (Time > NextTimestamp) { RebuildTree(false); // 1000 timers --> check each 150ms // 10000 timers --> check each 600ms // 100000 timers --> check each 5.1s const double WaitTimeSec = 0.1 + TimerNodes.Num() / 20000.0; const uint64 WaitTime = static_cast(WaitTimeSec / FPlatformTime::GetSecondsPerCycle64()); NextTimestamp = Time + WaitTime; } Aggregator->Tick(Session, InCurrentTime, InDeltaTime, [this]() { FinishAggregation(); }); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::RebuildTree(bool bResync) { FStopwatch SyncStopwatch; FStopwatch Stopwatch; Stopwatch.Start(); if (bResync) { TimerNodes.Empty(); } const uint32 PreviousNodeCount = TimerNodes.Num(); SyncStopwatch.Start(); if (Session.IsValid() && Trace::ReadTimingProfilerProvider(*Session.Get())) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::ITimingProfilerProvider& TimingProfilerProvider = *Trace::ReadTimingProfilerProvider(*Session.Get()); const Trace::ITimingProfilerTimerReader* TimerReader; TimingProfilerProvider.ReadTimers([&TimerReader](const Trace::ITimingProfilerTimerReader& Out) { TimerReader = &Out; }); const uint32 TimerCount = TimerReader->GetTimerCount(); if (TimerCount != PreviousNodeCount) { check(TimerCount > PreviousNodeCount); TimerNodes.Reserve(TimerCount); // Add nodes only for new timers. for (uint32 TimerIndex = PreviousNodeCount; TimerIndex < TimerCount; ++TimerIndex) { const Trace::FTimingProfilerTimer& Timer = *(TimerReader->GetTimer(TimerIndex)); ensure(Timer.Id == TimerIndex); const ETimerNodeType Type = Timer.IsGpuTimer ? ETimerNodeType::GpuScope : ETimerNodeType::CpuScope; FTimerNodePtr TimerNodePtr = MakeShared(Timer.Id, Timer.Name, Type); TimerNodes.Add(TimerNodePtr); } ensure(TimerNodes.Num() == TimerCount); } } SyncStopwatch.Stop(); if (bResync || TimerNodes.Num() != PreviousNodeCount) { // Disable sorting if too many items. if (TimerNodes.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 (FTimerNodePtr& NodePtr: SelectedItems) { NodePtr = GetTimerNode(NodePtr->GetTimerId()); } SelectedItems.RemoveAll([](const FTimerNodePtr& 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(TimingProfiler, Log, TEXT("[Timers] Tree view rebuilt in %.4fs (%.4fs + %.4fs) --> %d timers (%d added)"), TotalTime, SyncTime, TotalTime - SyncTime, TimerNodes.Num(), TimerNodes.Num() - PreviousNodeCount); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ResetStats() { Aggregator->Cancel(); Aggregator->SetTimeInterval(0.0, 0.0); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::UpdateStats(double StartTime, double EndTime) { Aggregator->Cancel(); Aggregator->SetTimeInterval(StartTime, EndTime); Aggregator->Start(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::FinishAggregation() { for (const FTimerNodePtr& NodePtr: TimerNodes) { NodePtr->ResetAggregatedStats(); } ApplyAggregation(Aggregator->GetResultTable()); Aggregator->ResetResults(); // Invalidate all tree table rows. for (const FTimerNodePtr& NodePtr: TimerNodes) { 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()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ApplyAggregation(Trace::ITable* AggregatedStatsTable) { if (AggregatedStatsTable) { TUniquePtr> TableReader(AggregatedStatsTable->CreateReader()); while (TableReader->IsValid()) { const Trace::FTimingProfilerAggregatedStats* Row = TableReader->GetCurrentRow(); FTimerNodePtr TimerNodePtr = GetTimerNode(Row->Timer->Id); if (TimerNodePtr) { TimerNodePtr->SetAggregatedStats(*Row); } TableReader->NextRow(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// FTimerNodePtr STimersView::GetTimerNode(uint32 TimerId) const { return (TimerId < (uint32)TimerNodes.Num()) ? TimerNodes[TimerId] : nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::SelectTimerNode(uint32 TimerId) { FTimerNodePtr NodePtr = GetTimerNode(TimerId); if (NodePtr) { TreeView->SetSelection(NodePtr); TreeView->RequestScrollIntoView(NodePtr); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ContextMenu_ExportTimingEvents_Execute() { if (!Session.IsValid()) return; FString SaveFilePath; IDesktopPlatform* Desktop = FDesktopPlatformModule::Get(); if (Desktop) { const Trace::IAnalysisSession* AnalysisSession = Session.Get(); if (!AnalysisSession) return; FString TraceName = FString(AnalysisSession->GetName()); FString DefaultCSV = TraceName + TEXT(".csv"); TArray OutFiles; const bool bOk = Desktop->SaveFileDialog( nullptr, TEXT("Export Timing Events (All)"), FPaths::ProjectSavedDir(), DefaultCSV, TEXT("CSV Files (*.csv)|*.csv"), 0, OutFiles); if (!bOk || OutFiles.Num() == 0) return; SaveFilePath = OutFiles[0]; } if (SaveFilePath.IsEmpty()) return; const Trace::IAnalysisSession* AnalysisSession = Session.Get(); if (!AnalysisSession) return; const bool bSuccess = FMinimalTimerExporter::ExportTimingEventsToCSV(*AnalysisSession, SaveFilePath, TraceTimer::FExportFilter{TEXT("ContextMenu_ExportTimingEvents_Execute")}); UE_LOG(LogTemp, Log, TEXT("Export Timing Events (All): %s %s"),bSuccess ? TEXT("Success") : TEXT("Failed"),*SaveFilePath); } bool STimersView::ContextMenu_ExportHierarchyJSON_CanExecute() const { return true; } void STimersView::ContextMenu_ExportHierarchyJSON_Execute() { if (!Session.IsValid()) return; FString SaveFilePath; IDesktopPlatform* Desktop = FDesktopPlatformModule::Get(); if (Desktop) { const Trace::IAnalysisSession* AnalysisSession = Session.Get(); if (!AnalysisSession) return; FString TraceName = FString(AnalysisSession->GetName()); FString DefaultJson = TraceName + TEXT(".json"); TArray OutFiles; const bool bOk = Desktop->SaveFileDialog( nullptr, TEXT("Export Frame Hierarchy JSON (Chunked)"), FPaths::ProjectSavedDir(), DefaultJson, TEXT("JSON Files (*.json)|*.json"), 0, OutFiles); if (!bOk || OutFiles.Num() == 0) return; SaveFilePath = OutFiles[0]; } const Trace::IAnalysisSession* AnalysisSession = Session.Get(); if (!AnalysisSession) return; FMinimalTimerExporter::ExportTimingEventsToJSON(*AnalysisSession, SaveFilePath, TraceTimer::FExportFilter{TEXT("ContextMenu_ExportTimingEvents_Execute")}); UE_LOG(LogTemp, Display, TEXT("Frame hierarchy JSON export done.")); } void STimersView::ToggleTimingViewEventFilter(FTimerNodePtr TimerNode) const { TSharedPtr Wnd = FTimingProfilerManager::Get()->GetProfilerWindow(); TSharedPtr TimingView = Wnd.IsValid() ? Wnd->GetTimingView() : nullptr; if (TimingView.IsValid()) { const uint64 EventType = static_cast(TimerNode->GetTimerId()); TimingView->ToggleEventFilterByEventType(EventType); } } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedPtr STimersView::GetTimingViewMainGraphTrack() const { TSharedPtr Wnd = FTimingProfilerManager::Get()->GetProfilerWindow(); TSharedPtr TimingView = Wnd.IsValid() ? Wnd->GetTimingView() : nullptr; return TimingView.IsValid() ? TimingView->GetMainTimingGraphTrack() : nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ToggleGraphSeries(TSharedRef GraphTrack, FTimerNodeRef NodePtr) const { const uint32 TimerId = NodePtr->GetTimerId(); TSharedPtr Series = GraphTrack->GetTimerSeries(TimerId); if (Series.IsValid()) { GraphTrack->RemoveTimerSeries(TimerId); GraphTrack->SetDirtyFlag(); NodePtr->SetAddedToGraphFlag(false); } else { GraphTrack->Show(); Series = GraphTrack->AddTimerSeries(TimerId, NodePtr->GetColor()); Series->SetName(FText::FromName(NodePtr->GetName())); GraphTrack->SetDirtyFlag(); NodePtr->SetAddedToGraphFlag(true); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::IsSeriesInTimingViewMainGraph(FTimerNodePtr TimerNode) const { TSharedPtr GraphTrack = GetTimingViewMainGraphTrack(); if (GraphTrack.IsValid()) { const uint32 TimerId = TimerNode->GetTimerId(); TSharedPtr Series = GraphTrack->GetTimerSeries(TimerId); return Series.IsValid(); } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ToggleTimingViewMainGraphEventSeries(FTimerNodePtr TimerNode) const { TSharedPtr GraphTrack = GetTimingViewMainGraphTrack(); if (GraphTrack.IsValid()) { ToggleGraphSeries(GraphTrack.ToSharedRef(), TimerNode.ToSharedRef()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool STimersView::ContextMenu_CopySelectedToClipboard_CanExecute() const { const TArray SelectedNodes = TreeView->GetSelectedItems(); return SelectedNodes.Num() > 0; } //////////////////////////////////////////////////////////////////////////////////////////////////// void STimersView::ContextMenu_CopySelectedToClipboard_Execute() { if (!Table->IsValid()) { return; } TArray SelectedNodes; for (FTimerNodePtr TimerPtr: TreeView->GetSelectedItems()) { SelectedNodes.Add(TimerPtr); } if (SelectedNodes.Num() == 0) { return; } FString ClipboardText; if (CurrentSorter.IsValid()) { CurrentSorter->Sort(SelectedNodes, ColumnSortMode == EColumnSortMode::Ascending ? Insights::ESortMode::Ascending : Insights::ESortMode::Descending); } Table->GetVisibleColumnsData(SelectedNodes, ClipboardText); if (ClipboardText.Len() > 0) { FPlatformApplicationMisc::ClipboardCopy(*ClipboardText); } } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply STimersView::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { return CommandList->ProcessCommandBindings(InKeyEvent) == true ? FReply::Handled() : FReply::Unhandled(); } //////////////////////////////////////////////////////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE