EM_Task/TraceInsights/Private/Insights/Widgets/STimersView.cpp
Boshuang Zhao 5144a49c9b add
2026-02-13 16:18:33 +08:00

2201 lines
87 KiB
C++

// 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 <limits>
#include "DesktopPlatformModule.h"
#include "IDesktopPlatform.h"
#include "Insights/ViewModels/MinimalTimerExporter.h"
#define LOCTEXT_NAMESPACE "STimersView"
////////////////////////////////////////////////////////////////////////////////////////////////////
STimersView::STimersView()
: Table(MakeShared<Insights::FTable>()), bExpansionSaved(false), bFilterOutZeroCountTimers(false), GroupingMode(ETimerGroupingMode::ByType), AvailableSorters(), CurrentSorter(nullptr), ColumnBeingSorted(GetDefaultColumnBeingSorted()), ColumnSortMode(GetDefaultColumnSortMode()), Aggregator(MakeShared<Insights::FTimerAggregator>())
{
FMemory::Memset(bTimerTypeIsVisible, 1);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
class FTimersViewCommands: public TCommands<FTimersViewCommands>
{
public:
FTimersViewCommands()
: TCommands<FTimersViewCommands>(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<FUICommandInfo> Command_CopyToClipboard;
};
void STimersView::InitCommandList()
{
FTimersViewCommands::Register();
CommandList = MakeShared<FUICommandList>();
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<TSharedPtr<ETimerGroupingMode>>)
.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<FTimerNodePtr>)
.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>(FTimerNodeTextFilter::FItemToStringArray::CreateSP(this, &STimersView::HandleItemToStringArray));
Filters = MakeShared<FTimerNodeFilterCollection>();
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<SWidget> STimersView::TreeView_GetMenuContent()
{
const TArray<FTimerNodePtr> SelectedNodes = TreeView->GetSelectedItems();
const int32 NumSelectedNodes = SelectedNodes.Num();
FTimerNodePtr SelectedNode = NumSelectedNodes ? SelectedNodes[0] : nullptr;
const TSharedPtr<Insights::FTableColumn> 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<STimingProfilerWindow> Wnd = FTimingProfilerManager::Get()->GetProfilerWindow();
TSharedPtr<STimingView> 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<STimingProfilerWindow> Wnd = FTimingProfilerManager::Get()->GetProfilerWindow();
TSharedPtr<STimingView> 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<FText>(),
TAttribute<FText>(),
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<SWidget> SCascadePreviewViewportToolBar::GenerateViewMenu() const
MenuBuilder.BeginSection("ColumnName", LOCTEXT("ContextMenu_Header_Misc_ColumnName", "Column Name"));
for (const TSharedRef<Insights::FTableColumn>& 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<Insights::FTableColumn>& 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<TSharedRef<Insights::FTableColumn>> Columns;
FTimersViewColumnFactory::CreateTimersViewColumns(Columns);
Table->SetColumns(Columns);
// Show columns.
for (const TSharedRef<Insights::FTableColumn>& 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<SWidget> 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<SWidget>)SNullWidget::NullWidget;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void STimersView::InsightsManager_OnSessionChanged()
{
TSharedPtr<const Trace::IAnalysisSession> 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<Insights::FBaseTreeNodePtr>& GroupChildren = GroupPtr->GetChildren();
int32 NumVisibleChildren = 0;
for (const Insights::FBaseTreeNodePtr& ChildPtr: GroupChildren)
{
const FTimerNodePtr& NodePtr = StaticCastSharedPtr<FTimerNode, Insights::FBaseTreeNode>(ChildPtr);
const bool bIsChildVisible = (!bFilterOutZeroCountTimers || NodePtr->GetAggregatedStats().InstanceCount > 0) && bTimerTypeIsVisible[static_cast<int>(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<double>::quiet_NaN();
AggregatedStats.AverageInclusiveTime = NanTimeValue;
AggregatedStats.MedianInclusiveTime = NanTimeValue;
AggregatedStats.AverageExclusiveTime = NanTimeValue;
AggregatedStats.MedianExclusiveTime = NanTimeValue;
const TArray<Insights::FBaseTreeNodePtr>& GroupChildren = GroupPtr->GetFilteredChildren();
for (const Insights::FBaseTreeNodePtr& ChildPtr: GroupChildren)
{
const FTimerNodePtr& NodePtr = StaticCastSharedPtr<FTimerNode, Insights::FBaseTreeNode>(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<FString>& OutSearchStrings) const
{
OutSearchStrings.Add(FTimerNodePtr->GetName().GetPlainNameString());
}
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedRef<SWidget> 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<int>(InStatType)] = (NewRadioState == ECheckBoxState::Checked);
ApplyFiltering();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
ECheckBoxState STimersView::FilterByTimerType_IsChecked(const ETimerNodeType InStatType) const
{
return bTimerTypeIsVisible[static_cast<int>(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<FTimerNodePtr> SelectedItems = TreeView->GetSelectedItems();
if (SelectedItems.Num() == 1 && !SelectedItems[0]->IsGroup())
{
FTimingProfilerManager::Get()->SetSelectedTimer(SelectedItems[0]->GetTimerId());
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void STimersView::TreeView_OnGetChildren(FTimerNodePtr InParent, TArray<FTimerNodePtr>& OutChildren)
{
const TArray<Insights::FBaseTreeNodePtr>& Children = InParent->GetFilteredChildren();
OutChildren.Reset(Children.Num());
for (const Insights::FBaseTreeNodePtr& Child: Children)
{
OutChildren.Add(StaticCastSharedPtr<FTimerNode, Insights::FBaseTreeNode>(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<ITableRow> STimersView::TreeView_OnGenerateRow(FTimerNodePtr NodePtr, const TSharedRef<STableViewBase>& OwnerTable)
{
TSharedRef<ITableRow> 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<Insights::FTable> InTablePtr, TSharedPtr<Insights::FTableColumn> 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<SHeaderRow::FColumn>& 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<FTimerNode>(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<FName, FTimerNodePtr> GroupNodeSet;
for (const FTimerNodePtr& NodePtr: TimerNodes)
{
const FName GroupName = NodePtr->GetMetaGroupName();
FTimerNodePtr GroupPtr = GroupNodeSet.FindRef(GroupName);
if (!GroupPtr)
{
GroupPtr = GroupNodeSet.Add(GroupName, MakeShared<FTimerNode>(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<ETimerNodeType, FTimerNodePtr> 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<FTimerNode>(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<TCHAR, FTimerNodePtr> 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<FTimerNode>(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<uint32, FTimerNodePtr> 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<FTimerNode>(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>(ETimerGroupingMode::Flat));
GroupByOptionsSource.Add(MakeShared<ETimerGroupingMode>(ETimerGroupingMode::ByName));
// GroupByOptionsSource.Add(MakeShared<ETimerGroupingMode>(ETimerGroupingMode::ByMetaGroupName));
GroupByOptionsSource.Add(MakeShared<ETimerGroupingMode>(ETimerGroupingMode::ByType));
GroupByOptionsSource.Add(MakeShared<ETimerGroupingMode>(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<ETimerGroupingMode> NewGroupingMode, ESelectInfo::Type SelectInfo)
{
if (SelectInfo != ESelectInfo::Direct)
{
GroupingMode = *NewGroupingMode;
CreateGroups();
SortTreeNodes();
ApplyFiltering();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedRef<SWidget> STimersView::GroupBy_OnGenerateWidget(TSharedPtr<ETimerGroupingMode> 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<Insights::FTableColumn>& ColumnRef: Table->GetColumns())
{
if (ColumnRef->CanBeSorted())
{
TSharedPtr<Insights::ITableCellValueSorter> SorterPtr = ColumnRef->GetValueSorter();
if (ensure(SorterPtr.IsValid()))
{
AvailableSorters.Add(SorterPtr);
}
}
}
UpdateCurrentSortingByColumn();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void STimersView::UpdateCurrentSortingByColumn()
{
TSharedPtr<Insights::FTableColumn> 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<FTimerNode>(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<float>())
.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<Insights::FTableColumn>& 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<FName> 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<Insights::FTableColumn>& 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<Insights::FTableColumn>& 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<FString> 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<FString> 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<uint64>(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<FTimerNode>(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<FTimerNodePtr> 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<ITableRow> TableRowPtr = TreeView->WidgetFromItem(NodePtr);
if (TableRowPtr.IsValid())
{
TSharedPtr<STimerTableRow> RowPtr = StaticCastSharedPtr<STimerTableRow, ITableRow>(TableRowPtr);
RowPtr->InvalidateContent();
}
}
UpdateTree(); // grouping + sorting + filtering
// Ensure the last selected item is visible.
const TArray<FTimerNodePtr> SelectedNodes = TreeView->GetSelectedItems();
if (SelectedNodes.Num() > 0)
{
TreeView->RequestScrollIntoView(SelectedNodes.Last());
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void STimersView::ApplyAggregation(Trace::ITable<Trace::FTimingProfilerAggregatedStats>* AggregatedStatsTable)
{
if (AggregatedStatsTable)
{
TUniquePtr<Trace::ITableReader<Trace::FTimingProfilerAggregatedStats>> 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<FString> 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<FString> 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<STimingProfilerWindow> Wnd = FTimingProfilerManager::Get()->GetProfilerWindow();
TSharedPtr<STimingView> TimingView = Wnd.IsValid() ? Wnd->GetTimingView() : nullptr;
if (TimingView.IsValid())
{
const uint64 EventType = static_cast<uint64>(TimerNode->GetTimerId());
TimingView->ToggleEventFilterByEventType(EventType);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedPtr<FTimingGraphTrack> STimersView::GetTimingViewMainGraphTrack() const
{
TSharedPtr<STimingProfilerWindow> Wnd = FTimingProfilerManager::Get()->GetProfilerWindow();
TSharedPtr<STimingView> TimingView = Wnd.IsValid() ? Wnd->GetTimingView() : nullptr;
return TimingView.IsValid() ? TimingView->GetMainTimingGraphTrack() : nullptr;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void STimersView::ToggleGraphSeries(TSharedRef<FTimingGraphTrack> GraphTrack, FTimerNodeRef NodePtr) const
{
const uint32 TimerId = NodePtr->GetTimerId();
TSharedPtr<FTimingGraphSeries> 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<FTimingGraphTrack> GraphTrack = GetTimingViewMainGraphTrack();
if (GraphTrack.IsValid())
{
const uint32 TimerId = TimerNode->GetTimerId();
TSharedPtr<FTimingGraphSeries> Series = GraphTrack->GetTimerSeries(TimerId);
return Series.IsValid();
}
return false;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void STimersView::ToggleTimingViewMainGraphEventSeries(FTimerNodePtr TimerNode) const
{
TSharedPtr<FTimingGraphTrack> GraphTrack = GetTimingViewMainGraphTrack();
if (GraphTrack.IsValid())
{
ToggleGraphSeries(GraphTrack.ToSharedRef(), TimerNode.ToSharedRef());
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool STimersView::ContextMenu_CopySelectedToClipboard_CanExecute() const
{
const TArray<FTimerNodePtr> SelectedNodes = TreeView->GetSelectedItems();
return SelectedNodes.Num() > 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void STimersView::ContextMenu_CopySelectedToClipboard_Execute()
{
if (!Table->IsValid())
{
return;
}
TArray<Insights::FBaseTreeNodePtr> 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