// Copyright Epic Games, Inc. All Rights Reserved. #include "Widgets/SProfilerWindow.h" #if STATS #include "Widgets/SBoxPanel.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SBox.h" #include "Widgets/SEventGraph.h" #include "SlateOptMacros.h" #include "Widgets/Layout/SSpacer.h" #include "Widgets/Images/SImage.h" #include "Styling/AppStyle.h" #include "Widgets/SProfilerToolbar.h" #include "Widgets/SFiltersAndPresets.h" #include "Widgets/SMultiDumpBrowser.h" #include "Widgets/SDataGraph.h" #include "Widgets/SProfilerMiniView.h" #include "Widgets/SProfilerGraphPanel.h" #include "Widgets/SProfilerSettings.h" #include "Widgets/Notifications/SNotificationList.h" #if WITH_EDITOR #include "AnalyticsEventAttribute.h" #include "Interfaces/IAnalyticsProvider.h" #include "EngineAnalytics.h" #endif // WITH_EDITOR #include "ProfilerStyle.h" #define LOCTEXT_NAMESPACE "SProfilerWindow" static FText GetTextForNotification( const EProfilerNotificationTypes NotificatonType, const ELoadingProgressStates ProgressState, const FString& Filename, const float ProgressPercent = 0.0f ) { FText Result; if( NotificatonType == EProfilerNotificationTypes::LoadingOfflineCapture ) { if( ProgressState == ELoadingProgressStates::Started ) { FFormatNamedArguments Args; Args.Add( TEXT("Filename"), FText::FromString( Filename ) ); Result = FText::Format( LOCTEXT("DescF_OfflineCapture_Started", "Started loading a file ../../{Filename}"), Args ); } else if( ProgressState == ELoadingProgressStates::InProgress ) { FFormatNamedArguments Args; Args.Add( TEXT("Filename"), FText::FromString( Filename ) ); Args.Add( TEXT("DataLoadingProgressPercent"), FText::AsPercent( ProgressPercent ) ); Result = FText::Format( LOCTEXT("DescF_OfflineCapture_InProgress", "Loading a file ../../{Filename} {DataLoadingProgressPercent}"), Args ); } else if( ProgressState == ELoadingProgressStates::Loaded ) { FFormatNamedArguments Args; Args.Add( TEXT("Filename"), FText::FromString( Filename ) ); Result = FText::Format( LOCTEXT("DescF_OfflineCapture_Loaded", "Capture file ../../{Filename} has been successfully loaded"), Args ); } else if( ProgressState == ELoadingProgressStates::Failed ) { FFormatNamedArguments Args; Args.Add( TEXT("Filename"), FText::FromString( Filename ) ); Result = FText::Format( LOCTEXT("DescF_OfflineCapture_Failed", "Failed to load capture file ../../{Filename}"), Args ); } } else if( NotificatonType == EProfilerNotificationTypes::SendingServiceSideCapture ) { if( ProgressState == ELoadingProgressStates::Started ) { FFormatNamedArguments Args; Args.Add( TEXT("Filename"), FText::FromString( Filename ) ); Result = FText::Format( LOCTEXT("DescF_ServiceSideCapture_Started", "Started receiving a file ../../{Filename}"), Args ); } else if( ProgressState == ELoadingProgressStates::InProgress ) { FFormatNamedArguments Args; Args.Add( TEXT("Filename"), FText::FromString( Filename ) ); Args.Add( TEXT("DataLoadingProgressPercent"), FText::AsPercent( ProgressPercent ) ); Result = FText::Format( LOCTEXT("DescF_ServiceSideCapture_InProgress", "Receiving a file ../../{Filename} {DataLoadingProgressPercent}"), Args ); } else if( ProgressState == ELoadingProgressStates::Loaded ) { FFormatNamedArguments Args; Args.Add( TEXT("Filename"), FText::FromString( Filename ) ); Result = FText::Format( LOCTEXT("DescF_ServiceSideCapture_Loaded", "Capture file ../../{Filename} has been successfully received"), Args ); } else if( ProgressState == ELoadingProgressStates::Failed ) { FFormatNamedArguments Args; Args.Add( TEXT("Filename"), FText::FromString( Filename ) ); Result = FText::Format( LOCTEXT("DescF_ServiceSideCapture_Failed", "Failed to receive capture file ../../{Filename}"), Args ); } } return Result; } SProfilerWindow::SProfilerWindow() : DurationActive(0.0f) {} SProfilerWindow::~SProfilerWindow() { // Remove ourselves from the profiler manager. if( FProfilerManager::Get().IsValid() ) { FProfilerManager::Get()->OnViewModeChanged().RemoveAll( this ); } #if WITH_EDITOR if (DurationActive > 0.0f && FEngineAnalytics::IsAvailable()) { FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.Profiler"), FAnalyticsEventAttribute(TEXT("Duration"), DurationActive)); } #endif // WITH_EDITOR } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SProfilerWindow::Construct( const FArguments& InArgs ) { // TODO: Cleanup overlay code. ChildSlot [ SNew(SOverlay) // Overlay slot for the main profiler window area, the first + SOverlay::Slot() [ SAssignNew(MainContentPanel, SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(0.0f) .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) [ SNew(SProfilerToolbar) ] ] /** Profiler Mini-view. */ + SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 6.0f, 0.0f, 0.0f) [ SNew(SBox) .HeightOverride(48.0f) .IsEnabled(this, &SProfilerWindow::IsProfilerEnabled) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(0.0f) .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) [ SAssignNew(ProfilerMiniView, SProfilerMiniView) ] ] ] + SVerticalBox::Slot() .FillHeight(1.0f) .Padding(0.0f, 4.0f, 0.0f, 0.0f) [ SNew(SSplitter) .Orientation(Orient_Horizontal) + SSplitter::Slot() .Value(0.25f) [ SNew(SSplitter) .Orientation(Orient_Vertical) + SSplitter::Slot() .Value(0.25f) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(SImage) .Image(FProfilerStyle::Get().GetBrush(TEXT("Profiler.Tab.FiltersAndPresets"))) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Text(LOCTEXT("MultiFileBrowser", "Stats dump browser")) ] ] + SVerticalBox::Slot() .AutoHeight() [ //SNew(STextBlock) //.Text(LOCTEXT("FileList", "File list here")) SAssignNew(MultiDumpBrowser, SMultiDumpBrowser) ] ] + SSplitter::Slot() .Expose(FiltersAndPresetsSlot) [ SNew(SVerticalBox) .IsEnabled(this, &SProfilerWindow::IsProfilerEnabled) // Header + SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(SImage) .Image(FProfilerStyle::Get().GetBrush(TEXT("Profiler.Tab.FiltersAndPresets"))) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Text( LOCTEXT("FiltersAndPresetsLabel", "Filters And Presets") ) ] ] // Filters And Presets + SVerticalBox::Slot() .FillHeight(1.0f) .Padding(0.0f, 2.0f, 0.0f, 0.0f) [ SAssignNew(FiltersAndPresets, SFiltersAndPresets) ] ] ] + SSplitter::Slot() .Value(0.75f) [ SNew(SSplitter) .Orientation(Orient_Vertical) //.PhysicalSplitterHandleSize( 2.0f ) //.HitDetectionSplitterHandleSize( 4.0f ) + SSplitter::Slot() .Value(0.25f) [ SNew(SVerticalBox) // Header +SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(SImage) .Image(FProfilerStyle::Get().GetBrush(TEXT("Profiler.Tab.GraphView"))) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Text(LOCTEXT("GraphViewLabel", "Graph View")) ] ] // Graph View + SVerticalBox::Slot() .FillHeight( 1.0f ) .Padding(0.0f, 2.0f, 0.0f, 0.0f) [ SAssignNew(GraphPanel, SProfilerGraphPanel) ] ] + SSplitter::Slot() .Value(0.75f) [ SAssignNew(EventGraphPanel, SVerticalBox) ] ] ] ] // session hint overlay + SOverlay::Slot() .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(SBorder) .BorderImage(FProfilerStyle::Get().GetBrush("NotificationList.ItemBackground")) .Padding(8.0f) .Visibility(this, &SProfilerWindow::IsSessionOverlayVissible) [ SNew(STextBlock) .Text(LOCTEXT("SelectSessionOverlayText", "Please select a session from the Session Browser or load a saved capture.")) ] ] // notification area overlay + SOverlay::Slot() .HAlign(HAlign_Right) .VAlign(VAlign_Bottom) .Padding(16.0f) [ SAssignNew(NotificationList, SNotificationList) ] // profiler settings overlay + SOverlay::Slot() .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Expose(OverlaySettingsSlot) ]; ProfilerMiniView->OnSelectionBoxChanged().AddSP( GraphPanel.ToSharedRef(), &SProfilerGraphPanel::MiniView_OnSelectionBoxChanged ); FProfilerManager::Get()->OnViewModeChanged().AddSP( this, &SProfilerWindow::ProfilerManager_OnViewModeChanged ); GraphPanel->ProfilerMiniView = ProfilerMiniView; } END_SLATE_FUNCTION_BUILD_OPTIMIZATION void SProfilerWindow::ManageEventGraphTab( const FGuid ProfilerInstanceID, const bool bCreateFakeTab, const FString TabName ) { // TODO: Add support for multiple instances. if( bCreateFakeTab ) { TSharedPtr EventGraphWidget; EventGraphPanel->ClearChildren(); // Header EventGraphPanel->AddSlot() .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SImage) .Image(FProfilerStyle::Get().GetBrush( TEXT("Profiler.Tab.EventGraph") ) ) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Text( FText::FromString(TabName) ) ] ]; EventGraphPanel->AddSlot() .AutoHeight() [ SNew( SSpacer ) .Size( FVector2D( 2.0f, 2.0f ) ) ]; // Event graph EventGraphPanel->AddSlot() .FillHeight( 1.0f ) [ SAssignNew(EventGraphWidget,SEventGraph) ]; ActiveEventGraphs.Add( ProfilerInstanceID, EventGraphWidget.ToSharedRef() ); // Register data graph with the new event graph tab. EventGraphWidget->OnEventGraphRestoredFromHistory().AddSP( GraphPanel->GetMainDataGraph().Get(), &SDataGraph::EventGraph_OnRestoredFromHistory ); } else { ActiveEventGraphs.Remove( ProfilerInstanceID ); } } void SProfilerWindow::UpdateEventGraph( const FGuid ProfilerInstanceID, const FEventGraphDataRef AverageEventGraph, const FEventGraphDataRef MaximumEventGraph, bool bInitial ) { const auto* EventGraph = ActiveEventGraphs.Find( ProfilerInstanceID ); if( EventGraph ) { (*EventGraph)->SetNewEventGraphState( AverageEventGraph, MaximumEventGraph, bInitial ); } } EVisibility SProfilerWindow::IsSessionOverlayVissible() const { if( FProfilerManager::Get()->HasValidSession() ) { return EVisibility::Hidden; } return EVisibility::Visible; } bool SProfilerWindow::IsProfilerEnabled() const { const bool bIsActive = FProfilerManager::Get()->IsConnected() || FProfilerManager::Get()->IsCaptureFileFullyProcessed(); return bIsActive; } void SProfilerWindow::ManageLoadingProgressNotificationState( const FString& Filename, const EProfilerNotificationTypes NotificatonType, const ELoadingProgressStates ProgressState, const float DataLoadingProgress ) { const FString BaseFilename = FPaths::GetBaseFilename( Filename ); if( ProgressState == ELoadingProgressStates::Started ) { const bool bContains = ActiveNotifications.Contains( Filename ); if( !bContains ) { FNotificationInfo NotificationInfo( GetTextForNotification(NotificatonType,ProgressState,BaseFilename) ); NotificationInfo.bFireAndForget = false; NotificationInfo.bUseLargeFont = false; // Add two buttons, one for cancel, one for loading the received file. if( NotificatonType == EProfilerNotificationTypes::SendingServiceSideCapture ) { const FText CancelButtonText = LOCTEXT("CancelButton_Text", "Cancel"); const FText CancelButtonTT = LOCTEXT("CancelButton_TTText", "Hides this notification"); const FText LoadButtonText = LOCTEXT("LoadButton_Text", "Load file"); const FText LoadButtonTT = LOCTEXT("LoadButton_TTText", "Loads the received file and hides this notification"); NotificationInfo.ButtonDetails.Add( FNotificationButtonInfo( CancelButtonText, CancelButtonTT, FSimpleDelegate::CreateSP( this, &SProfilerWindow::SendingServiceSideCapture_Cancel, Filename ), SNotificationItem::CS_Success ) ); NotificationInfo.ButtonDetails.Add( FNotificationButtonInfo( LoadButtonText, LoadButtonTT, FSimpleDelegate::CreateSP( this, &SProfilerWindow::SendingServiceSideCapture_Load, Filename ), SNotificationItem::CS_Success ) ); } SNotificationItemWeak LoadingProgress = NotificationList->AddNotification(NotificationInfo); LoadingProgress.Pin()->SetCompletionState( SNotificationItem::CS_Pending ); ActiveNotifications.Add( Filename, LoadingProgress ); } } else if( ProgressState == ELoadingProgressStates::InProgress ) { const SNotificationItemWeak* LoadingProgressPtr = ActiveNotifications.Find( Filename ); if( LoadingProgressPtr ) { TSharedPtr LoadingProcessPinned = LoadingProgressPtr->Pin(); LoadingProcessPinned->SetText( GetTextForNotification( NotificatonType, ProgressState, BaseFilename, DataLoadingProgress ) ); LoadingProcessPinned->SetCompletionState( SNotificationItem::CS_Pending ); } } else if( ProgressState == ELoadingProgressStates::Loaded ) { const SNotificationItemWeak* LoadingProgressPtr = ActiveNotifications.Find( Filename ); if( LoadingProgressPtr ) { TSharedPtr LoadingProcessPinned = LoadingProgressPtr->Pin(); LoadingProcessPinned->SetText( GetTextForNotification( NotificatonType, ProgressState, BaseFilename ) ); LoadingProcessPinned->SetCompletionState( SNotificationItem::CS_Success ); // Notifications for received files are handled by ty user. if( NotificatonType == EProfilerNotificationTypes::LoadingOfflineCapture ) { LoadingProcessPinned->ExpireAndFadeout(); ActiveNotifications.Remove( Filename ); } } } else if( ProgressState == ELoadingProgressStates::Failed ) { const SNotificationItemWeak* LoadingProgressPtr = ActiveNotifications.Find( Filename ); if( LoadingProgressPtr ) { TSharedPtr LoadingProcessPinned = LoadingProgressPtr->Pin(); LoadingProcessPinned->SetText( GetTextForNotification(NotificatonType, ProgressState, BaseFilename) ); LoadingProcessPinned->SetCompletionState(SNotificationItem::CS_Fail); LoadingProcessPinned->ExpireAndFadeout(); ActiveNotifications.Remove( Filename ); } } else if (ProgressState == ELoadingProgressStates::Cancelled) { const SNotificationItemWeak* LoadingProgressPtr = ActiveNotifications.Find( Filename ); if ( LoadingProgressPtr ) { TSharedPtr LoadingProcessPinned = LoadingProgressPtr->Pin(); LoadingProcessPinned->ExpireAndFadeout(); ActiveNotifications.Remove( Filename ); } } } void SProfilerWindow::SendingServiceSideCapture_Cancel( const FString Filename ) { const SNotificationItemWeak* LoadingProgressPtr = ActiveNotifications.Find( Filename ); if( LoadingProgressPtr ) { TSharedPtr LoadingProcessPinned = LoadingProgressPtr->Pin(); LoadingProcessPinned->ExpireAndFadeout(); ActiveNotifications.Remove( Filename ); } } void SProfilerWindow::SendingServiceSideCapture_Load( const FString Filename ) { const SNotificationItemWeak* LoadingProgressPtr = ActiveNotifications.Find( Filename ); if( LoadingProgressPtr ) { TSharedPtr LoadingProcessPinned = LoadingProgressPtr->Pin(); LoadingProcessPinned->ExpireAndFadeout(); ActiveNotifications.Remove( Filename ); // TODO: Move to a better place. const FString PathName = FPaths::ProfilingDir() / TEXT("UnrealStats/Received/"); const FString StatFile = FPaths::GetCleanFilename(Filename); const FString StatFilepath = PathName / StatFile; FProfilerManager::Get()->LoadProfilerCapture( StatFilepath ); } } EActiveTimerReturnType SProfilerWindow::UpdateActiveDuration( double InCurrentTime, float InDeltaTime ) { DurationActive += InDeltaTime; // The profiler window will explicitly unregister this active timer when the mouse leaves return EActiveTimerReturnType::Continue; } void SProfilerWindow::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { SCompoundWidget::OnMouseEnter(MyGeometry, MouseEvent); if (!ActiveTimerHandle.IsValid()) { ActiveTimerHandle = RegisterActiveTimer(0.f, FWidgetActiveTimerDelegate::CreateSP(this, &SProfilerWindow::UpdateActiveDuration)); } } void SProfilerWindow::OnMouseLeave(const FPointerEvent& MouseEvent) { SCompoundWidget::OnMouseLeave(MouseEvent); auto PinnedActiveTimerHandle = ActiveTimerHandle.Pin(); if (PinnedActiveTimerHandle.IsValid()) { UnRegisterActiveTimer(PinnedActiveTimerHandle.ToSharedRef()); } } FReply SProfilerWindow::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ) { return FProfilerManager::Get()->GetCommandList()->ProcessCommandBindings( InKeyEvent ) ? FReply::Handled() : FReply::Unhandled(); } FReply SProfilerWindow::OnDragOver( const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent ) { TSharedPtr DragDropOp = DragDropEvent.GetOperationAs(); if(DragDropOp.IsValid()) { if( DragDropOp->HasFiles() ) { const TArray& Files = DragDropOp->GetFiles(); if( Files.Num() == 1 ) { const FString DraggedFileExtension = FPaths::GetExtension( Files[0], true ); if( DraggedFileExtension == FStatConstants::StatsFileExtension ) { return FReply::Handled(); } else if( DraggedFileExtension == FStatConstants::StatsFileRawExtension ) { return FReply::Handled(); } } } } return SCompoundWidget::OnDragOver(MyGeometry,DragDropEvent); } FReply SProfilerWindow::OnDrop( const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent ) { TSharedPtr DragDropOp = DragDropEvent.GetOperationAs(); if(DragDropOp.IsValid()) { if( DragDropOp->HasFiles() ) { // For now, only allow a single file. const TArray& Files = DragDropOp->GetFiles(); if( Files.Num() == 1 ) { const FString DraggedFileExtension = FPaths::GetExtension( Files[0], true ); if( DraggedFileExtension == FStatConstants::StatsFileExtension ) { // Enqueue load operation. FProfilerManager::Get()->LoadProfilerCapture( Files[0] ); return FReply::Handled(); } else if( DraggedFileExtension == FStatConstants::StatsFileRawExtension ) { // Enqueue load operation. FProfilerManager::Get()->LoadRawStatsFile( Files[0] ); return FReply::Handled(); } } } } return SCompoundWidget::OnDrop(MyGeometry,DragDropEvent); } void SProfilerWindow::OpenProfilerSettings() { MainContentPanel->SetEnabled( false ); (*OverlaySettingsSlot) [ SNew(SBorder) .BorderImage(FProfilerStyle::Get().GetBrush("NotificationList.ItemBackground") ) .Padding( 8.0f ) [ SNew(SProfilerSettings) .OnClose( this, &SProfilerWindow::CloseProfilerSettings ) .SettingPtr( &FProfilerManager::GetSettings() ) ] ]; } void SProfilerWindow::CloseProfilerSettings() { // Close the profiler settings by simply replacing widget with a null one. (*OverlaySettingsSlot) [ SNullWidget::NullWidget ]; MainContentPanel->SetEnabled( true ); } void SProfilerWindow::ProfilerManager_OnViewModeChanged( EProfilerViewMode NewViewMode ) { if( NewViewMode == EProfilerViewMode::LineIndexBased ) { EventGraphPanel->SetVisibility( EVisibility::Visible ); EventGraphPanel->SetEnabled( true ); (*FiltersAndPresetsSlot) [ FiltersAndPresets.ToSharedRef() ]; } else if( NewViewMode == EProfilerViewMode::ThreadViewTimeBased ) { EventGraphPanel->SetVisibility( EVisibility::Collapsed ); EventGraphPanel->SetEnabled( false ); (*FiltersAndPresetsSlot) [ SNullWidget::NullWidget ]; } } #undef LOCTEXT_NAMESPACE #endif // STATS