// Copyright Epic Games, Inc. All Rights Reserved. #include "UnrealInsightsLauncher.h" #include "EditorTraceUtilities.h" #include "Async/TaskGraphInterfaces.h" #include "Styling/AppStyle.h" #include "IUATHelperModule.h" #include "Logging/LogMacros.h" #include "Logging/MessageLog.h" #include "MessageLogModule.h" #include "ToolMenus.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include "ProfilingDebugging/TraceAuxiliary.h" #include "Trace/StoreClient.h" #define LOCTEXT_NAMESPACE "FUnrealInsightsLauncher" TSharedPtr FUnrealInsightsLauncher::Instance = nullptr; // We use this Task to launch notifications from the Game Thread because on Mac the app is closed if the notification API is called from another thread. class FLogMessageOnGameThreadTask { public: FLogMessageOnGameThreadTask(const FText& InMessage) : Message(InMessage) {} FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FLogMessageOnGameThreadTask, STATGROUP_TaskGraphTasks); } ENamedThreads::Type GetDesiredThread() { return ENamedThreads::Type::GameThread; } static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; } void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) { FUnrealInsightsLauncher::Get()->LogMessage(Message); } private: FText Message; }; class FLiveSessionQueryTask { public: FLiveSessionQueryTask(TSharedPtr InOutData) : OutData(InOutData) {} FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FLiveSessionQueryTask, STATGROUP_TaskGraphTasks); } ENamedThreads::Type GetDesiredThread() { return ENamedThreads::Type::AnyThread; } static ESubsequentsMode::Type GetSubsequentsMode() { return ESubsequentsMode::TrackSubsequents; } void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) { using namespace UE::Trace; FStoreClient* StoreClient = FStoreClient::Connect(TEXT("localhost")); OutData->TaskLiveSessionData.Empty(); if (!StoreClient) { return; } uint32 SessionCount = StoreClient->GetSessionCount(); if (!SessionCount) { return; } OutData->StorePort = StoreClient->GetStorePort(); for (uint32 Index = 0; Index < SessionCount; ++Index) { const FStoreClient::FSessionInfo* SessionInfo = StoreClient->GetSessionInfo(Index); if (!SessionInfo) { continue; } uint32 TraceId = SessionInfo->GetTraceId(); const FStoreClient::FTraceInfo* Info = StoreClient->GetTraceInfoById(TraceId); if (Info) { OutData->TaskLiveSessionData.Add(FString(Info->GetName()), TraceId); } } } TSharedPtr OutData; }; FUnrealInsightsLauncher::FUnrealInsightsLauncher() : LogListingName(TEXT("UnrealInsights")) { } FUnrealInsightsLauncher::~FUnrealInsightsLauncher() { } FString FUnrealInsightsLauncher::GetInsightsApplicationPath() { FString Path = FPlatformProcess::GenerateApplicationPath(TEXT("UnrealInsights"), EBuildConfiguration::Development); return FPaths::ConvertRelativePathToFull(Path); } void FUnrealInsightsLauncher::StartUnrealInsights(const FString& Path, const FString& Parameters) { auto Callback = [](const EStartInsightsResult Result) {}; StartUnrealInsights(Path, Parameters, Callback); } void FUnrealInsightsLauncher::StartUnrealInsights(const FString& Path, const FString& Parameters, StartUnrealInsightsCallback Callback) { if (!FPaths::FileExists(Path)) { BuildUnrealInsights(Path, Parameters, Callback); return; } constexpr bool bLaunchDetached = true; constexpr bool bLaunchHidden = false; constexpr bool bLaunchReallyHidden = false; uint32 ProcessID = 0; const int32 PriorityModifier = 0; const TCHAR* OptionalWorkingDirectory = nullptr; void* PipeWriteChild = nullptr; void* PipeReadChild = nullptr; UnrealInsightsHandle = FPlatformProcess::CreateProc(*Path, *Parameters, bLaunchDetached, bLaunchHidden, bLaunchReallyHidden, &ProcessID, PriorityModifier, OptionalWorkingDirectory, PipeWriteChild, PipeReadChild); if (UnrealInsightsHandle.IsValid()) { UE_LOG(LogTraceUtilities, Log, TEXT("Launched Unreal Insights executable: %s %s"), *Path, *Parameters); Callback(EStartInsightsResult::Completed); } else { const FText MessageBoxTextFmt = LOCTEXT("ExecutableNotFound_TextFmt", "Could not start Unreal Insights executable at path: {0}"); const FText MessageBoxText = FText::Format(MessageBoxTextFmt, FText::FromString(Path)); LogMessageOnGameThread(MessageBoxText); Callback(EStartInsightsResult::LaunchFailed); } } void FUnrealInsightsLauncher::CloseUnrealInsights() { if (UnrealInsightsHandle.IsValid()) { FPlatformProcess::TerminateProc(UnrealInsightsHandle); } else { UE_LOG(LogTraceUtilities, Log, TEXT("Could not find the Unreal Insights process handler")); } } void FUnrealInsightsLauncher::BuildUnrealInsights(const FString& Path, const FString& LaunchParameters, StartUnrealInsightsCallback Callback) { UE_LOG(LogTraceUtilities, Log, TEXT("Could not find the Unreal Insights executable: %s. Attempting to build UnrealInsights."), *Path); FString Arguments; #if PLATFORM_WINDOWS FText PlatformName = LOCTEXT("PlatformName_Windows", "Windows"); Arguments = TEXT("BuildTarget -Target=UnrealInsights -Platform=Win64"); #elif PLATFORM_MAC FText PlatformName = LOCTEXT("PlatformName_Mac", "Mac"); Arguments = TEXT("BuildTarget -Target=UnrealInsights -Platform=Mac"); #elif PLATFORM_LINUX FText PlatformName = LOCTEXT("PlatformName_Linux", "Linux"); Arguments = TEXT("BuildTarget -Target=UnrealInsights -Platform=Linux"); #endif IUATHelperModule::Get().CreateUatTask(Arguments, PlatformName, LOCTEXT("BuildingUnrealInsights", "Building Unreal Insights"), LOCTEXT("BuildUnrealInsightsTask", "Build Unreal Insights Task"), FAppStyle::GetBrush(TEXT("MainFrame.CookContent")), nullptr, [this, Path, LaunchParameters, Callback](FString Result, double Time) { if (Result.Equals(TEXT("Completed"))) { #if PLATFORM_MAC // On Mac we genereate the path again so that it includes the newly built executable. FString NewPath = GetInsightsApplicationPath(); this->StartUnrealInsights(NewPath, LaunchParameters, Callback); #else this->StartUnrealInsights(Path, LaunchParameters, Callback); #endif } else { Callback(EStartInsightsResult::BuildFailed); } }); } void FUnrealInsightsLauncher::LogMessage(const FText& Message) { FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); if (!MessageLogModule.IsRegisteredLogListing(LogListingName)) { MessageLogModule.RegisterLogListing(LogListingName, LOCTEXT("UnrealInsights", "Unreal Insights")); } FMessageLog ReportMessageLog(LogListingName); TSharedRef TokenizedMessage = FTokenizedMessage::Create(EMessageSeverity::Error, Message); ReportMessageLog.AddMessage(TokenizedMessage); ReportMessageLog.Notify(); } void FUnrealInsightsLauncher::LogMessageOnGameThread(const FText& Message) { TGraphTask::CreateTask().ConstructAndDispatchWhenReady(Message); } bool FUnrealInsightsLauncher::TryOpenTraceFromDestination(const FString& Destination) { TRACE_CPUPROFILER_EVENT_SCOPE(TryOpenTraceFromDestination); if (Destination.IsEmpty()) return false; if (FPaths::GetExtension(Destination).Equals(TEXT("utrace"))) { if (FPaths::FileExists(Destination)) { return OpenTraceFile(Destination); } return false; } // assume it's a ip address of the target server for now so we don't introduce dependencies on the socket system //if (ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->GetAddressFromString(Destination)) return OpenActiveTraceFromStore(Destination); } bool FUnrealInsightsLauncher::OpenTraceFile(const FString& FilePath) { if (!FilePath.StartsWith(TEXT("\"")) && !FilePath.EndsWith(TEXT("\""))) { TStringBuilder<1024> StringBuilder; StringBuilder.AppendChar(TEXT('"')); StringBuilder.Append(FilePath); StringBuilder.AppendChar(TEXT('"')); StartUnrealInsights(GetInsightsApplicationPath(), StringBuilder.ToString()); return true; } StartUnrealInsights(GetInsightsApplicationPath(), FilePath); return true; } bool FUnrealInsightsLauncher::OpenRemoteTrace(const FString& TraceHostAddress, const uint16 TraceHostPort, uint32 TraceID) { TStringBuilder<512> ParamsSB; ParamsSB += TEXT("-Store="); ParamsSB += TraceHostAddress; ParamsSB += TEXT(":"); ParamsSB.Appendf(TEXT("%u"), TraceHostPort); ParamsSB += TEXT(" -OpenTraceID="); ParamsSB.Appendf(TEXT("%u"), TraceID); StartUnrealInsights(GetInsightsApplicationPath(), ParamsSB.ToString()); return true; } bool FUnrealInsightsLauncher::OpenActiveTraceFromStore(const FString& TraceHostAddress) { TRACE_CPUPROFILER_EVENT_SCOPE(OpenActiveTraceFromStore); UE::Trace::FStoreClient* StoreClient = UE::Trace::FStoreClient::Connect(*TraceHostAddress); if (!StoreClient) { const FText MessageBoxTextFmt = LOCTEXT("StoreClientConnectFailed_TextFmt", "Could not connect to StoreClient at {0}"); const FText MessageBoxText = FText::Format(MessageBoxTextFmt, FText::FromString(TraceHostAddress)); LogMessage(MessageBoxText); //No active connection to store client return false; } int SessionCount = StoreClient->GetSessionCount(); if (!SessionCount) { const FText MessageBoxTextFmt = LOCTEXT("StoreClientNoSession_TextFmt", "No active session found in StoreClient at {0}"); const FText MessageBoxText = FText::Format(MessageBoxTextFmt, FText::FromString(TraceHostAddress)); LogMessage(MessageBoxText); return false; } // Get first active session FGuid SessionGuid, TraceGuid; if (!FTraceAuxiliary::IsConnected(SessionGuid, TraceGuid)) { return false; } // TraceGuid is enough to identify the session const UE::Trace::FStoreClient::FSessionInfo* SessionInfo = StoreClient->GetSessionInfoByGuid(TraceGuid); // Fallback to old method of using last active session, in case running with old server if (!SessionInfo) { UE_LOG(LogTraceUtilities, Warning, TEXT("Unable to find session using guid. Possibly running with older server version. Falling back to last active session.")); SessionInfo = StoreClient->GetSessionInfo(0); } if (!SessionInfo) { // Failed to retrieve SessionInfo for active Session with Index 0 const FText MessageBoxTextFmt = LOCTEXT("StoreClientFailedToRetrieve_TextFmt", "Failed to retrieve active session in StoreClient at {0}"); const FText MessageBoxText = FText::Format(MessageBoxTextFmt, FText::FromString(TraceHostAddress)); LogMessage(MessageBoxText); return false; } uint32 TraceId = SessionInfo->GetTraceId(); if (TraceId == 0 || !OpenRemoteTrace(TraceHostAddress, (uint16)StoreClient->GetStorePort(), TraceId)) { const FText MessageBoxTextFmt = LOCTEXT("StoreClientFailedToOpenRemote_TextFmt", "Failed to open remote session from address {0} with TraceId: {1}"); const FText MessageBoxText = FText::Format(MessageBoxTextFmt, FText::FromString(TraceHostAddress), FText::AsNumber(TraceId)); LogMessage(MessageBoxText); return false; } return true; } FLiveSessionTracker::FLiveSessionTracker() { TaskLiveSessionData = MakeShared(); } void FLiveSessionTracker::Update() { if (bIsQueryInProgress && Event.IsValid() && Event->IsComplete()) { bIsQueryInProgress = false; LiveSessionMap = TaskLiveSessionData->TaskLiveSessionData; StorePort = TaskLiveSessionData->StorePort; if (!LiveSessionMap.IsEmpty()) { bHasData = true; } } } void FLiveSessionTracker::StartQuery() { if (!bIsQueryInProgress) { Event = TGraphTask::CreateTask().ConstructAndDispatchWhenReady(TaskLiveSessionData); bIsQueryInProgress = true; } } const FLiveSessionsMap& FLiveSessionTracker::GetLiveSessions() { Update(); return LiveSessionMap; } bool FLiveSessionTracker::HasData() { Update(); return bHasData; } uint32 FLiveSessionTracker::GetStorePort() { Update(); return StorePort; } #undef LOCTEXT_NAMESPACE