// Copyright Epic Games, Inc. All Rights Reserved. #include "PerformanceMonitor.h" #include "Input/Reply.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Engine/GameViewportClient.h" #include "Widgets/SCompoundWidget.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SWindow.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Images/SImage.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Input/SButton.h" #include "Widgets/SToolTip.h" #include "Framework/Docking/TabManager.h" #include "Styling/AppStyle.h" #include "Editor/EditorPerProjectUserSettings.h" #include "EngineGlobals.h" #include "Editor.h" #include "SScalabilitySettings.h" #include "ShaderCompiler.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "Settings/EditorSettings.h" #include "Editor/EditorPerformanceSettings.h" #include "EditorViewportClient.h" #define LOCTEXT_NAMESPACE "PerformanceMonitor" const double AutoApplyScalabilityTimeout = 10; /** Scalability dialog widget */ class SScalabilitySettingsDialog : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SScalabilitySettingsDialog) {} SLATE_ARGUMENT(FOnClicked, OnDoneClicked) SLATE_END_ARGS() public: /** Construct this widget */ void Construct(const FArguments& InArgs) { ChildSlot [ SNew(SBorder) .HAlign(HAlign_Fill) .BorderImage(FAppStyle::GetBrush("ChildWindow.Background")) [ SNew(SBox) .WidthOverride(500.0f) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(5.f) [ SNew(STextBlock) .Text(LOCTEXT("PerformanceWarningDescription", "The current performance of the editor seems to be low.\nUse the options below to reduce the amount of detail and increase performance.")) ] + SVerticalBox::Slot() .AutoHeight() .Padding(5.f) [ SNew(SScalabilitySettings) ] + SVerticalBox::Slot() .AutoHeight() .Padding(5.f) [ SNew(STextBlock) .ToolTip( SNew(SToolTip) [ SNew(SImage) .Image(FAppStyle::GetBrush("Scalability.ScalabilitySettings")) ] ) .AutoWrapText(true) .Text(LOCTEXT("PerformanceWarningChangeLater", "You can modify these settings in future via \"Quick Settings\" button on the level editor toolbar and choosing \"Engine Scalability Settings\".")) ] + SVerticalBox::Slot() .AutoHeight() .Padding(5.f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0) .HAlign(HAlign_Right) [ SNew(SButton) .Text(LOCTEXT("ScalabilityDone", "Done")) .OnClicked(InArgs._OnDoneClicked) ] ] ] ] ]; } }; static TAutoConsoleVariable CVarFineSampleTime( TEXT("PerfWarn.FineSampleTime"), 30, TEXT("How many seconds we sample the percentage for the fine-grained minimum FPS."), ECVF_Default); static TAutoConsoleVariable CVarCoarseSampleTime( TEXT("PerfWarn.CoarseSampleTime"), 600, TEXT("How many seconds we sample the percentage for the coarse-grained minimum FPS."), ECVF_Default); static TAutoConsoleVariable CVarFineMinFPS( TEXT("PerfWarn.FineMinFPS"), 10, TEXT("The FPS threshold below which we warn about for fine-grained sampling."), ECVF_Default); static TAutoConsoleVariable CVarCoarseMinFPS( TEXT("PerfWarn.CoarseMinFPS"), 20, TEXT("The FPS threshold below which we warn about for coarse-grained sampling."), ECVF_Default); static TAutoConsoleVariable CVarFinePercentThreshold( TEXT("PerfWarn.FinePercentThreshold"), 80, TEXT("The percentage of samples that fall below min FPS above which we warn for."), ECVF_Default); static TAutoConsoleVariable CVarCoarsePercentThreshold( TEXT("PerfWarn.CoarsePercentThreshold"), 80, TEXT("The percentage of samples that fall below min FPS above which we warn for."), ECVF_Default); FPerformanceMonitor::FPerformanceMonitor() { LastEnableTime = 0; NotificationTimeout = AutoApplyScalabilityTimeout; bIsNotificationAllowed = true; CVarDelegate = FConsoleCommandDelegate::CreateLambda([this]{ static const auto CVarFine = IConsoleManager::Get().FindConsoleVariable(TEXT("PerfWarn.FineSampleTime")); static const auto CVarCoarse = IConsoleManager::Get().FindConsoleVariable(TEXT("PerfWarn.CoarseSampleTime")); static int32 LastTickFine = -1, LastTickCoarse = -1; if (CVarFine->GetInt() != LastTickFine || CVarCoarse->GetInt() != LastTickCoarse) { LastTickFine = CVarFine->GetInt(); LastTickCoarse = CVarCoarse->GetInt(); UpdateMovingAverageSamplers(); } }); CVarDelegateHandle = IConsoleManager::Get().RegisterConsoleVariableSink_Handle(CVarDelegate); } FPerformanceMonitor::~FPerformanceMonitor() { IConsoleManager::Get().UnregisterConsoleVariableSink_Handle(CVarDelegateHandle); } bool FPerformanceMonitor::WillAutoScalabilityHelp() const { Scalability::FQualityLevels CurrentLevels = Scalability::GetQualityLevels(); Scalability::FQualityLevels NewLevels = GetAutoScalabilityQualityLevels(); bool IsAutoScaleLower = false; IsAutoScaleLower |= NewLevels.ResolutionQuality < CurrentLevels.ResolutionQuality; IsAutoScaleLower |= NewLevels.ViewDistanceQuality < CurrentLevels.ViewDistanceQuality; IsAutoScaleLower |= NewLevels.AntiAliasingQuality < CurrentLevels.AntiAliasingQuality; IsAutoScaleLower |= NewLevels.ShadowQuality < CurrentLevels.ShadowQuality; IsAutoScaleLower |= NewLevels.GlobalIlluminationQuality < CurrentLevels.GlobalIlluminationQuality; IsAutoScaleLower |= NewLevels.ReflectionQuality < CurrentLevels.ReflectionQuality; IsAutoScaleLower |= NewLevels.PostProcessQuality < CurrentLevels.PostProcessQuality; IsAutoScaleLower |= NewLevels.TextureQuality < CurrentLevels.TextureQuality; IsAutoScaleLower |= NewLevels.EffectsQuality < CurrentLevels.EffectsQuality; IsAutoScaleLower |= NewLevels.FoliageQuality < CurrentLevels.FoliageQuality; IsAutoScaleLower |= NewLevels.ShadingQuality < CurrentLevels.ShadingQuality; // We don't check things like real-time, because the user may have enabled it temporarily. return IsAutoScaleLower; } Scalability::FQualityLevels FPerformanceMonitor::GetAutoScalabilityQualityLevels() const { const Scalability::FQualityLevels ExistingLevels = Scalability::GetQualityLevels(); Scalability::FQualityLevels NewLevels = GetDefault()->EngineBenchmarkResult; // Make sure we don't turn settings up if the user has turned them down for any reason NewLevels.ResolutionQuality = FMath::Min(NewLevels.ResolutionQuality, ExistingLevels.ResolutionQuality); NewLevels.ViewDistanceQuality = FMath::Min(NewLevels.ViewDistanceQuality, ExistingLevels.ViewDistanceQuality); NewLevels.AntiAliasingQuality = FMath::Min(NewLevels.AntiAliasingQuality, ExistingLevels.AntiAliasingQuality); NewLevels.ShadowQuality = FMath::Min(NewLevels.ShadowQuality, ExistingLevels.ShadowQuality); NewLevels.GlobalIlluminationQuality = FMath::Min(NewLevels.GlobalIlluminationQuality, ExistingLevels.GlobalIlluminationQuality); NewLevels.ReflectionQuality = FMath::Min(NewLevels.ReflectionQuality, ExistingLevels.ReflectionQuality); NewLevels.PostProcessQuality = FMath::Min(NewLevels.PostProcessQuality, ExistingLevels.PostProcessQuality); NewLevels.TextureQuality = FMath::Min(NewLevels.TextureQuality, ExistingLevels.TextureQuality); NewLevels.EffectsQuality = FMath::Min(NewLevels.EffectsQuality, ExistingLevels.EffectsQuality); NewLevels.FoliageQuality = FMath::Min(NewLevels.FoliageQuality, ExistingLevels.FoliageQuality); NewLevels.ShadingQuality = FMath::Min(NewLevels.ShadingQuality, ExistingLevels.ShadingQuality); return NewLevels; } void FPerformanceMonitor::AutoApplyScalability() { GetMutableDefault()->AutoApplyScalabilityBenchmark(); Scalability::FQualityLevels NewLevels = FPerformanceMonitor::GetAutoScalabilityQualityLevels(); Scalability::SetQualityLevels(NewLevels); Scalability::SaveState(GEditorSettingsIni); GEditor->RedrawAllViewports(); const bool bAutoApplied = false; Scalability::RecordQualityLevelsAnalytics(bAutoApplied); for (FEditorViewportClient* VC : GEditor->GetAllViewportClients()) { if (VC) { VC->SetRealtime(false); VC->Invalidate(); } } // Reset the timers so as not to skew the data with the time it took to do the benchmark FineMovingAverage.Reset(); CoarseMovingAverage.Reset(); } void FPerformanceMonitor::ShowPerformanceWarning(FText MessageText) { static double MinNotifyTime = 30; if ((FPlatformTime::Seconds() - LastEnableTime) > MinNotifyTime) { // Only show a new one if we've not shown one for a while LastEnableTime = FPlatformTime::Seconds(); NotificationTimeout = AutoApplyScalabilityTimeout; // Create notification item FNotificationInfo Info(MessageText); Info.bFireAndForget = false; Info.FadeOutDuration = 3.0f; Info.ExpireDuration = 0.0f; Info.bUseLargeFont = false; Info.ButtonDetails.Add( FNotificationButtonInfo( LOCTEXT("ApplyNow", "Apply Now"), FText::GetEmpty(), FSimpleDelegate::CreateRaw( this, &FPerformanceMonitor::AutoApplyScalability ) ) ); Info.ButtonDetails.Add( FNotificationButtonInfo( LOCTEXT("TweakManually", "Tweak Manually"), FText::GetEmpty(), FSimpleDelegate::CreateRaw( this, &FPerformanceMonitor::ShowScalabilityDialog ) ) ); Info.ButtonDetails.Add( FNotificationButtonInfo( LOCTEXT("DontRemindMe", "Cancel & Ignore"), FText::GetEmpty(), FSimpleDelegate::CreateRaw( this, &FPerformanceMonitor::CancelPerformanceNotification ) ) ); PerformanceWarningNotificationPtr = FSlateNotificationManager::Get().AddNotification(Info); PerformanceWarningNotificationPtr.Pin()->SetCompletionState(SNotificationItem::CS_Pending); } } void FPerformanceMonitor::CancelPerformanceNotification() { UEditorPerformanceSettings* EditorUserSettings = GetMutableDefault(); EditorUserSettings->bMonitorEditorPerformance = false; EditorUserSettings->PostEditChange(); EditorUserSettings->SaveConfig(); Reset(); } void FPerformanceMonitor::HidePerformanceWarning() { // Finished! Notify the UI. TSharedPtr NotificationItem = PerformanceWarningNotificationPtr.Pin(); if (NotificationItem.IsValid()) { NotificationItem->SetCompletionState(SNotificationItem::CS_Success); NotificationItem->Fadeout(); PerformanceWarningNotificationPtr.Reset(); } } void FPerformanceMonitor::Tick(float DeltaTime) { if (GEngine->ShouldThrottleCPUUsage() && (!GShaderCompilingManager || !GShaderCompilingManager->IsCompiling() ) ) { return; } extern ENGINE_API float GAverageFPS; FineMovingAverage.Tick(FPlatformTime::Seconds(), GAverageFPS); CoarseMovingAverage.Tick(FPlatformTime::Seconds(), GAverageFPS); bool bMonitorEditorPerformance = GetDefault()->bMonitorEditorPerformance && FApp::IsBenchmarking() == false; if( !bMonitorEditorPerformance || !bIsNotificationAllowed ) { return; } FFormatNamedArguments Arguments; bool bLowFramerate = false; int32 SampleTime = 0; float PercentUnderTarget = 0.f; if (FineMovingAverage.IsReliable()) { static IConsoleVariable* CVarMinFPS = IConsoleManager::Get().FindConsoleVariable(TEXT("PerfWarn.FineMinFPS")); static IConsoleVariable* CVarPercentThreshold = IConsoleManager::Get().FindConsoleVariable(TEXT("PerfWarn.FinePercentThreshold")); static IConsoleVariable* CVarSampleTime = IConsoleManager::Get().FindConsoleVariable(TEXT("PerfWarn.FineSampleTime")); PercentUnderTarget = FineMovingAverage.PercentageBelowThreshold(CVarMinFPS->GetFloat()); if (PercentUnderTarget >= CVarPercentThreshold->GetFloat()) { Arguments.Add(TEXT("Framerate"), CVarMinFPS->GetInt()); Arguments.Add(TEXT("Percentage"), FMath::FloorToFloat(PercentUnderTarget)); SampleTime = CVarSampleTime->GetInt(); bLowFramerate = true; } } if (!bLowFramerate && CoarseMovingAverage.IsReliable()) { static IConsoleVariable* CVarMinFPS = IConsoleManager::Get().FindConsoleVariable(TEXT("PerfWarn.CoarseMinFPS")); static IConsoleVariable* CVarPercentThreshold = IConsoleManager::Get().FindConsoleVariable(TEXT("PerfWarn.CoarsePercentThreshold")); PercentUnderTarget = CoarseMovingAverage.PercentageBelowThreshold(CVarMinFPS->GetFloat()); if (PercentUnderTarget >= CVarPercentThreshold->GetFloat()) { static IConsoleVariable* CoarseSampleTimeCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("PerfWarn.CoarseSampleTime")); Arguments.Add(TEXT("Framerate"), CVarMinFPS->GetInt()); Arguments.Add(TEXT("Percentage"), FMath::FloorToFloat(PercentUnderTarget)); SampleTime = CoarseSampleTimeCVar->GetInt(); bLowFramerate = true; } } auto AlreadyOpenItem = PerformanceWarningNotificationPtr.Pin(); if (!bLowFramerate) { // Framerate is back up again - just reset everything and hide the timeout if (AlreadyOpenItem.IsValid()) { Reset(); } } else { if( GetDefault()->IsScalabilityBenchmarkValid() ) { return; } // Choose an appropriate message enum MessagesEnum { Seconds, SecondsPercent, Minute, MinutePercent, Minutes, MinutesPercent }; const FText Messages[] = { LOCTEXT("PerformanceWarningInProgress_Seconds", "Your framerate has been under {Framerate} FPS for the past {SampleTime} seconds.\n\nDo you want to apply reduced quality settings? {TimeRemaining}s"), LOCTEXT("PerformanceWarningInProgress_Seconds_Percent", "Your framerate has been under {Framerate} FPS for {Percentage}% of the past {SampleTime} seconds.\n\nDo you want to apply reduced quality settings? {TimeRemaining}s"), LOCTEXT("PerformanceWarningInProgress_Minute", "Your framerate has been under {Framerate} FPS for the past minute.\n\nDo you want to apply reduced quality settings? {TimeRemaining}s"), LOCTEXT("PerformanceWarningInProgress_Minute_Percent", "Your framerate has been under {Framerate} FPS for {Percentage}% of the last minute.\n\nDo you want to apply reduced quality settings? {TimeRemaining}s"), LOCTEXT("PerformanceWarningInProgress_Minutes", "Your framerate has been below {Framerate} FPS for the past {SampleTime} minutes.\n\nDo you want to apply reduced quality settings? {TimeRemaining}s"), LOCTEXT("PerformanceWarningInProgress_Minutes_Percent", "Your framerate has been below {Framerate} FPS for {Percentage}% of the past {SampleTime} minutes.\n\nDo you want to apply reduced quality settings? {TimeRemaining}s"), }; int32 Message; if (SampleTime < 60) { Arguments.Add(TEXT("SampleTime"), SampleTime); Message = Seconds; } else if (SampleTime == 60) { Message = Minute; } else { Arguments.Add(TEXT("SampleTime"), SampleTime / 60); Message = Minutes; } // Use the message with the specific percentage on if applicable if (PercentUnderTarget <= 95.f) { ++Message; } // Now update the notification or create a new one if (AlreadyOpenItem.IsValid()) { NotificationTimeout -= DeltaTime; Arguments.Add(TEXT("TimeRemaining"), FMath::Max(1, FMath::CeilToInt(NotificationTimeout))); if (NotificationTimeout <= 0) { // Timed-out. Apply the settings. Reset(); bIsNotificationAllowed = false; } else { AlreadyOpenItem->SetText(FText::Format(Messages[Message], Arguments)); } } else { NotificationTimeout = AutoApplyScalabilityTimeout; Arguments.Add(TEXT("TimeRemaining"), int(NotificationTimeout)); ShowPerformanceWarning(FText::Format(Messages[Message], Arguments)); } } } void FPerformanceMonitor::Reset() { FineMovingAverage.Reset(); CoarseMovingAverage.Reset(); HidePerformanceWarning(); bIsNotificationAllowed = true; } void FPerformanceMonitor::UpdateMovingAverageSamplers() { static const int NumberOfSamples = 50; IConsoleManager& Console = IConsoleManager::Get(); float SampleTime = Console.FindConsoleVariable(TEXT("PerfWarn.FineSampleTime"))->GetFloat(); FineMovingAverage = FMovingAverage(NumberOfSamples, SampleTime / NumberOfSamples); SampleTime = Console.FindConsoleVariable(TEXT("PerfWarn.CoarseSampleTime"))->GetFloat(); CoarseMovingAverage = FMovingAverage(NumberOfSamples, SampleTime / NumberOfSamples); } void FPerformanceMonitor::ShowScalabilityDialog() { Reset(); bIsNotificationAllowed = false; auto ExistingWindow = ScalabilitySettingsWindowPtr.Pin(); if (ExistingWindow.IsValid()) { ExistingWindow->BringToFront(); } else { // Create the window ScalabilitySettingsWindowPtr = ExistingWindow = SNew(SWindow) .Title(LOCTEXT("PerformanceWarningDialogTitle", "Scalability Options")) .SupportsMaximize(false) .SupportsMinimize(false) .CreateTitleBar(true) .SizingRule(ESizingRule::Autosized); ExistingWindow->SetOnWindowClosed(FOnWindowClosed::CreateStatic([](const TSharedRef&, FPerformanceMonitor* PerfWarn){ PerfWarn->Reset(); }, this)); ExistingWindow->SetContent( SNew(SScalabilitySettingsDialog) .OnDoneClicked( FOnClicked::CreateStatic([](TWeakPtr Window, FPerformanceMonitor* PerfWarn){ auto WindowPin = Window.Pin(); if (WindowPin.IsValid()) { PerfWarn->bIsNotificationAllowed = true; WindowPin->RequestDestroyWindow(); } return FReply::Handled(); }, ScalabilitySettingsWindowPtr, this) ) ); TSharedPtr RootWindow = FGlobalTabmanager::Get()->GetRootWindow(); if (RootWindow.IsValid()) { FSlateApplication::Get().AddModalWindow(ExistingWindow.ToSharedRef(), RootWindow); } else { FSlateApplication::Get().AddWindow(ExistingWindow.ToSharedRef()); } } } #undef LOCTEXT_NAMESPACE