Files
UnrealEngine/Engine/Source/Developer/ProfilerClient/Private/ProfilerClientManager.cpp
2025-05-18 13:04:45 +08:00

1174 lines
37 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ProfilerClientManager.h"
#include "HAL/FileManager.h"
#include "MessageEndpointBuilder.h"
#include "Misc/Paths.h"
#include "Misc/SecureHash.h"
#include "Async/Async.h"
#include "Serialization/MemoryReader.h"
#include "ProfilerServiceMessages.h"
DEFINE_LOG_CATEGORY_STATIC(LogProfilerClient, Log, All);
DECLARE_CYCLE_STAT(TEXT("HandleDataReceived"), STAT_PC_HandleDataReceived, STATGROUP_Profiler);
DECLARE_CYCLE_STAT(TEXT("ReadStatMessages"), STAT_PC_ReadStatMessages, STATGROUP_Profiler);
DECLARE_CYCLE_STAT(TEXT("AddStatMessages"), STAT_PC_AddStatMessages, STATGROUP_Profiler);
DECLARE_CYCLE_STAT(TEXT("GenerateDataFrame"), STAT_PC_GenerateDataFrame, STATGROUP_Profiler);
DECLARE_CYCLE_STAT(TEXT("AddStatFName"), STAT_PC_AddStatFName, STATGROUP_Profiler);
DECLARE_CYCLE_STAT(TEXT("AddGroupFName"), STAT_PC_AddGroupFName, STATGROUP_Profiler);
DECLARE_CYCLE_STAT(TEXT("GenerateCycleGraph"), STAT_PC_GenerateCycleGraph, STATGROUP_Profiler);
DECLARE_CYCLE_STAT(TEXT("GenerateAccumulator"),STAT_PC_GenerateAccumulator, STATGROUP_Profiler);
DECLARE_CYCLE_STAT(TEXT("FindOrAddStat"), STAT_PC_FindOrAddStat, STATGROUP_Profiler);
DECLARE_CYCLE_STAT(TEXT("FindOrAddThread"), STAT_PC_FindOrAddThread, STATGROUP_Profiler);
UE::Tasks::FPipe FProfilerClientManager::AsyncTaskPipe{ TEXT("SessionProfilerClientPipe") };
/* FProfilerClientManager structors
*****************************************************************************/
FProfilerClientManager::FProfilerClientManager(const TSharedRef<IMessageBus, ESPMode::ThreadSafe>& InMessageBus)
{
#if STATS
MessageBus = InMessageBus;
MessageEndpoint = FMessageEndpoint::Builder("FProfilerClientModule", InMessageBus)
.Handling<FProfilerServiceAuthorize>(this, &FProfilerClientManager::HandleServiceAuthorizeMessage)
.Handling<FProfilerServiceData2>(this, &FProfilerClientManager::HandleProfilerServiceData2Message)
.Handling<FProfilerServicePreviewAck>(this, &FProfilerClientManager::HandleServicePreviewAckMessage)
.Handling<FProfilerServiceFileChunk>(this, &FProfilerClientManager::HandleServiceFileChunk)
.Handling<FProfilerServicePing>(this, &FProfilerClientManager::HandleServicePingMessage);
if (MessageEndpoint.IsValid())
{
OnShutdownMessageBusDelegateHandle = InMessageBus->OnShutdown().AddRaw(this, &FProfilerClientManager::HandleMessageBusShutdown);
MessageEndpoint->Subscribe<FProfilerServicePing>();
}
TickDelegate = FTickerDelegate::CreateRaw(this, &FProfilerClientManager::HandleTicker);
MessageDelegate = FTickerDelegate::CreateRaw(this, &FProfilerClientManager::HandleMessagesTicker);
LastPingTime = FDateTime::Now();
RetryTime = 5.f;
bIsLivePreview = false;
LoadConnection = nullptr;
MessageDelegateHandle = FTSTicker::GetCoreTicker().AddTicker(MessageDelegate);
#endif
}
FProfilerClientManager::~FProfilerClientManager()
{
#if STATS
Shutdown();
Unsubscribe();
if (MessageBus.IsValid())
{
MessageBus->OnShutdown().Remove(OnShutdownMessageBusDelegateHandle);
}
LoadConnection = nullptr;
#endif
}
/* IProfilerClient interface
*****************************************************************************/
void FProfilerClientManager::Subscribe(const FGuid& Session)
{
#if STATS
FGuid OldSessionId = ActiveSessionId;
PendingSessionId = Session;
if (MessageEndpoint.IsValid())
{
if (OldSessionId.IsValid())
{
TArray<FGuid> Instances;
Connections.GenerateKeyArray(Instances);
for (int32 i = 0; i < Instances.Num(); ++i)
{
MessageEndpoint->Publish(FMessageEndpoint::MakeMessage<FProfilerServiceUnsubscribe>(OldSessionId, Instances[i]), EMessageScope::Network);
// fire the disconnection delegate
ProfilerClientDisconnectedDelegate.Broadcast(ActiveSessionId, Instances[i]);
}
ActiveSessionId.Invalidate();
}
ActiveSessionId = PendingSessionId;
}
Connections.Reset();
UE_LOG(LogProfilerClient, Log, TEXT("Subscribe Session: %s"), *Session.ToString());
#endif
}
void FProfilerClientManager::Track(const FGuid& Instance)
{
#if STATS
if (MessageEndpoint.IsValid() && ActiveSessionId.IsValid() && !PendingInstances.Contains(Instance))
{
PendingInstances.Add(Instance);
MessageEndpoint->Publish(FMessageEndpoint::MakeMessage<FProfilerServiceSubscribe>(ActiveSessionId, Instance), EMessageScope::Network);
RetryTime = 5.f;
TickDelegateHandle = FTSTicker::GetCoreTicker().AddTicker(TickDelegate, RetryTime);
UE_LOG(LogProfilerClient, Verbose, TEXT("Track Session: %s, Instance: %s"), *ActiveSessionId.ToString(), *Instance.ToString());
}
#endif
}
void FProfilerClientManager::Untrack(const FGuid& Instance)
{
#if STATS
if (MessageEndpoint.IsValid() && ActiveSessionId.IsValid())
{
MessageEndpoint->Publish(FMessageEndpoint::MakeMessage<FProfilerServiceUnsubscribe>(ActiveSessionId, Instance), EMessageScope::Network);
Connections.Remove(Instance);
// fire the disconnection delegate
ProfilerClientDisconnectedDelegate.Broadcast(ActiveSessionId, Instance);
UE_LOG(LogProfilerClient, Verbose, TEXT("Untrack Session: %s, Instance: %s"), *ActiveSessionId.ToString(), *Instance.ToString());
}
#endif
}
void FProfilerClientManager::Unsubscribe()
{
#if STATS
PendingSessionId.Invalidate();
Subscribe(PendingSessionId);
#endif
}
void FProfilerClientManager::SetCaptureState(const bool bRequestedCaptureState, const FGuid& InstanceId /*= FGuid()*/ )
{
#if STATS
if (MessageEndpoint.IsValid() && ActiveSessionId.IsValid())
{
if(!InstanceId.IsValid())
{
TArray<FMessageAddress> Instances;
for (auto It = Connections.CreateConstIterator(); It; ++It)
{
Instances.Add(It.Value().ProfilerServiceAddress);
}
MessageEndpoint->Send(FMessageEndpoint::MakeMessage<FProfilerServiceCapture>(bRequestedCaptureState), Instances);
UE_LOG(LogProfilerClient, Verbose, TEXT("SetCaptureState Session: %s, Instance: %s, State: %i"), *ActiveSessionId.ToString(), *InstanceId.ToString(), (int32)bRequestedCaptureState);
}
else
{
const FMessageAddress& MessageAddress = Connections.Find(InstanceId)->ProfilerServiceAddress;
MessageEndpoint->Send(FMessageEndpoint::MakeMessage<FProfilerServiceCapture>(bRequestedCaptureState), MessageAddress);
UE_LOG(LogProfilerClient, Verbose, TEXT("SetCaptureState Session: %s, Instance: %s, State: %i"), *ActiveSessionId.ToString(), *InstanceId.ToString(), (int32)bRequestedCaptureState);
}
}
#endif
}
void FProfilerClientManager::SetPreviewState(const bool bRequestedPreviewState, const FGuid& InstanceId /*= FGuid()*/)
{
#if STATS
if (MessageEndpoint.IsValid() && ActiveSessionId.IsValid())
{
bIsLivePreview = bRequestedPreviewState;
if(!InstanceId.IsValid())
{
TArray<FMessageAddress> Instances;
for (auto It = Connections.CreateConstIterator(); It; ++It)
{
Instances.Add(It.Value().ProfilerServiceAddress);
}
MessageEndpoint->Send(FMessageEndpoint::MakeMessage<FProfilerServicePreview>(bRequestedPreviewState), Instances);
UE_LOG(LogProfilerClient, Verbose, TEXT("SetPreviewState Session: %s, Instance: %s, State: %i"), *ActiveSessionId.ToString(), *InstanceId.ToString(), (int32)bRequestedPreviewState);
}
else
{
const FMessageAddress& MessageAddress = Connections.Find(InstanceId)->ProfilerServiceAddress;
MessageEndpoint->Send(FMessageEndpoint::MakeMessage<FProfilerServicePreview>(bRequestedPreviewState), MessageAddress);
UE_LOG(LogProfilerClient, Verbose, TEXT("SetPreviewState Session: %s, Instance: %s, State: %i"), *ActiveSessionId.ToString(), *InstanceId.ToString(), (int32)bRequestedPreviewState);
}
}
#endif
}
#if STATS
/*-----------------------------------------------------------------------------
New read test, still temporary, but around 4x faster
-----------------------------------------------------------------------------*/
class FNewStatsReader : public FStatsReadFile
{
friend struct FStatsReader<FNewStatsReader>;
typedef FStatsReadFile Super;
public:
/** Initialize. */
void Initialize(FProfilerClientManager* InProfilerClientManager, FServiceConnection* InLoadConnection)
{
ProfilerClientManager = InProfilerClientManager;
LoadConnection = InLoadConnection;
}
protected:
/** Initialization constructor. */
FNewStatsReader(const TCHAR* InFilename)
: FStatsReadFile(InFilename, false)
, ProfilerClientManager(nullptr)
, LoadConnection(nullptr)
{
// Keep only the last frame.
SetHistoryFrames(1);
}
/** Called every each frame has been read from the file. */
virtual void ReadStatsFrame(const TArray<FStatMessage>& CondensedMessages, const int64 Frame) override
{
SCOPE_CYCLE_COUNTER(STAT_PC_GenerateDataFrame);
FProfilerDataFrame& DataFrame = LoadConnection->CurrentData;
DataFrame.Frame = static_cast<uint32>(Frame);
DataFrame.FrameStart = 0.0;
DataFrame.CountAccumulators.Reset();
DataFrame.CycleGraphs.Reset();
DataFrame.FloatAccumulators.Reset();
DataFrame.MetaDataUpdated = false;
// Get the stat stack root and the non frame stats.
FRawStatStackNode Stack;
TArray<FStatMessage> NonFrameStats;
State.UncondenseStackStats(CondensedMessages, Stack, nullptr, &NonFrameStats);
LoadConnection->GenerateCycleGraphs(Stack, DataFrame.CycleGraphs);
LoadConnection->GenerateAccumulators(NonFrameStats, DataFrame.CountAccumulators, DataFrame.FloatAccumulators);
// Create a copy of the stats metadata.
FStatMetaData* MetaDataPtr = nullptr;
if (DataFrame.MetaDataUpdated)
{
MetaDataPtr = new FStatMetaData();
*MetaDataPtr = LoadConnection->StatMetaData;
}
// Create a copy of the stats data.
FProfilerDataFrame* DataFramePtr = new FProfilerDataFrame();
*DataFramePtr = DataFrame;
// Send to game thread.
FSimpleDelegateGraphTask::CreateAndDispatchWhenReady
(
FSimpleDelegateGraphTask::FDelegate::CreateRaw(ProfilerClientManager, &FProfilerClientManager::SendProfilerDataFrameToGame, DataFramePtr, MetaDataPtr, LoadConnection->InstanceId),
TStatId(), nullptr, ENamedThreads::GameThread
);
}
/** Called after reading all data from the file. */
virtual void PreProcessStats() override
{
FSimpleDelegateGraphTask::CreateAndDispatchWhenReady
(
FSimpleDelegateGraphTask::FDelegate::CreateRaw(ProfilerClientManager, &FProfilerClientManager::FinalizeLoading, LoadConnection->InstanceId),
TStatId(), NULL, ENamedThreads::GameThread
);
}
FProfilerClientManager* ProfilerClientManager;
FServiceConnection* LoadConnection;
};
#endif
void FServiceConnection::LoadCapture(const FString& DataFilepath, FProfilerClientManager* ProfilerClientManager)
{
#if STATS
StatsReader = FStatsReader<FNewStatsReader>::Create(*DataFilepath);
if (StatsReader)
{
StatsReader->Initialize(ProfilerClientManager, this);
StatsReader->ReadAndProcessAsynchronously();
}
#endif
}
void FProfilerClientManager::LoadCapture(const FString& DataFilepath, const FGuid& ProfileId)
{
#if STATS
// Start an async load.
LoadConnection = &Connections.FindOrAdd(ProfileId);
LoadConnection->InstanceId = ProfileId;
LoadConnection->StatMetaData.SecondsPerCycle = FPlatformTime::GetSecondsPerCycle(); // fix this by adding a message which specifies this
ProfilerLoadStartedDelegate.Broadcast(ProfileId);
LoadConnection->LoadCapture(DataFilepath, this);
RetryTime = 0.05f;
TickDelegateHandle = FTSTicker::GetCoreTicker().AddTicker(TickDelegate, RetryTime);
#endif
}
void FProfilerClientManager::RequestLastCapturedFile(const FGuid& InstanceId /*= FGuid()*/)
{
#if STATS
if (MessageEndpoint.IsValid() && ActiveSessionId.IsValid())
{
if(!InstanceId.IsValid())
{
TArray<FMessageAddress> Instances;
for (auto It = Connections.CreateConstIterator(); It; ++It)
{
Instances.Add(It.Value().ProfilerServiceAddress);
}
MessageEndpoint->Send(FMessageEndpoint::MakeMessage<FProfilerServiceRequest>(EProfilerRequestType::PRT_SendLastCapturedFile), Instances);
}
else
{
const FMessageAddress& MessageAddress = Connections.Find(InstanceId)->ProfilerServiceAddress;
MessageEndpoint->Send(FMessageEndpoint::MakeMessage<FProfilerServiceRequest>(EProfilerRequestType::PRT_SendLastCapturedFile), MessageAddress);
}
}
#endif
}
const FStatMetaData& FProfilerClientManager::GetStatMetaData(const FGuid& InstanceId) const
{
return Connections.Find(InstanceId)->StatMetaData;
}
FProfilerClientDataDelegate& FProfilerClientManager::OnProfilerData()
{
return ProfilerDataDelegate;
}
FProfilerFileTransferDelegate& FProfilerClientManager::OnProfilerFileTransfer()
{
return ProfilerFileTransferDelegate;
}
FProfilerClientConnectedDelegate& FProfilerClientManager::OnProfilerClientConnected()
{
return ProfilerClientConnectedDelegate;
}
FProfilerClientDisconnectedDelegate& FProfilerClientManager::OnProfilerClientDisconnected()
{
return ProfilerClientDisconnectedDelegate;
}
FProfilerMetaDataUpdateDelegate& FProfilerClientManager::OnMetaDataUpdated()
{
return ProfilerMetaDataUpdatedDelegate;
}
FProfilerLoadStartedDelegate& FProfilerClientManager::OnLoadStarted()
{
return ProfilerLoadStartedDelegate;
}
FProfilerLoadCompletedDelegate& FProfilerClientManager::OnLoadCompleted()
{
return ProfilerLoadCompletedDelegate;
}
FProfilerLoadCancelledDelegate& FProfilerClientManager::OnLoadCancelled()
{
return ProfilerLoadCancelledDelegate;
}
/* FProfilerClientManager event handlers
*****************************************************************************/
void FProfilerClientManager::HandleMessageBusShutdown()
{
#if STATS
Shutdown();
MessageEndpoint.Reset();
MessageBus.Reset();
#endif
}
void FProfilerClientManager::HandleServiceAuthorizeMessage(const FProfilerServiceAuthorize& Message, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& Context)
{
#if STATS
if (ActiveSessionId == Message.SessionId && PendingInstances.Contains(Message.InstanceId))
{
PendingInstances.Remove(Message.InstanceId);
FServiceConnection& Connection = Connections.FindOrAdd(Message.InstanceId);
Connection.Initialize(Message, Context);
// Fire a meta data update message
//ProfilerMetaDataUpdatedDelegate.Broadcast(Message.InstanceId);
// Fire the client connection event
ProfilerClientConnectedDelegate.Broadcast(ActiveSessionId, Message.InstanceId);
UE_LOG(LogProfilerClient, Verbose, TEXT("Authorize SessionId: %s, InstanceId: %s"), *Message.SessionId.ToString(), *Message.InstanceId.ToString());
}
#endif
}
/* FServiceConnection
*****************************************************************************/
FServiceConnection::FServiceConnection()
: StatsReader(nullptr)
{ }
FServiceConnection::~FServiceConnection()
{
#if STATS
if (StatsReader)
{
StatsReader->RequestStop();
while (StatsReader->IsBusy())
{
FPlatformProcess::Sleep(2.0f);
UE_LOG(LogStats, Log, TEXT("RequestStop: Stage: %s / %3i%%"), *StatsReader->GetProcessingStageAsString(), StatsReader->GetStageProgress());
}
delete StatsReader;
StatsReader = nullptr;
}
#endif
for (const auto& It : ReceivedData)
{
delete It.Value;
}
}
void FServiceConnection::Initialize(const FProfilerServiceAuthorize& Message, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& Context)
{
#if STATS
ProfilerServiceAddress = Context->GetSender();
InstanceId = Message.InstanceId;
CurrentData.Frame = 0;
#endif
}
bool FProfilerClientManager::CheckHashAndWrite(const FProfilerServiceFileChunk& FileChunk, const FProfilerFileChunkHeader& FileChunkHeader, FArchive* Writer)
{
#if STATS
const int32 HashSize = 20;
uint8 LocalHash[HashSize]={0};
// De-hex string into TArray<uint8>
TArray<uint8> FileChunkData;
const int32 DataLength = FileChunk.HexData.Len() / 2;
FileChunkData.Reset(DataLength);
FileChunkData.AddUninitialized(DataLength);
FString::ToHexBlob(FileChunk.HexData, FileChunkData.GetData(), DataLength);
// Hash file chunk data.
FSHA1 Sha;
Sha.Update(FileChunkData.GetData(), FileChunkHeader.ChunkSize);
// Hash file chunk header.
Sha.Update(FileChunk.Header.GetData(), FileChunk.Header.Num());
Sha.Final();
Sha.GetHash(LocalHash);
const int32 MemDiff = FMemory::Memcmp(FileChunk.ChunkHash.GetData(), LocalHash, HashSize);
bool bResult = false;
if(MemDiff == 0)
{
// Write the data to the archive.
Writer->Seek(FileChunkHeader.ChunkOffset);
Writer->Serialize((void*)FileChunkData.GetData(), FileChunkHeader.ChunkSize);
bResult = true;
}
return bResult;
#else
return false;
#endif
}
void FProfilerClientManager::HandleServiceFileChunk(const FProfilerServiceFileChunk& FileChunk, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& Context)
{
#if STATS
LLM_SCOPE_BYNAME(TEXT("SessionProfiler"));
const TCHAR* StrTmp = TEXT(".tmp");
// Read file chunk header.
FMemoryReader Reader(FileChunk.Header);
FProfilerFileChunkHeader FileChunkHeader;
Reader << FileChunkHeader;
FileChunkHeader.Validate();
const bool bValidFileChunk = !FailedTransfer.Contains(FileChunk.Filename);
if (ActiveSessionId.IsValid() && Connections.Find(FileChunk.InstanceId) != nullptr && bValidFileChunk)
{
FReceivedFileInfo* ReceivedFileInfo = ActiveTransfers.Find(FileChunk.Filename);
if(!ReceivedFileInfo)
{
const FString PathName = FPaths::ProfilingDir() + TEXT("UnrealStats/Received/");
const FString StatFilepath = PathName + FPaths::GetCleanFilename((const FString&)FileChunk.Filename) + StrTmp;
UE_LOG(LogProfilerClient, Log, TEXT("Opening stats file for service-client sending: %s"), *StatFilepath);
FArchive* FileWriter = IFileManager::Get().CreateFileWriter(*StatFilepath);
if(!FileWriter)
{
UE_LOG(LogProfilerClient, Error, TEXT("Could not open: %s"), *StatFilepath);
return;
}
ReceivedFileInfo = &ActiveTransfers.Add(FileChunk.Filename, FReceivedFileInfo(FileWriter,0,StatFilepath));
ProfilerFileTransferDelegate.Broadcast(FileChunk.Filename, ReceivedFileInfo->Progress, FileChunkHeader.FileSize);
}
const bool bSimulateBadFileChunk = true;//FMath::Rand() % 10 != 0;
const bool bSuccess = CheckHashAndWrite(FileChunk, FileChunkHeader, ReceivedFileInfo->FileWriter) && bSimulateBadFileChunk;
if(bSuccess)
{
ReceivedFileInfo->Progress += FileChunkHeader.ChunkSize;
ReceivedFileInfo->Update();
if(ReceivedFileInfo->Progress == FileChunkHeader.FileSize)
{
// File has been successfully sent, so send this information to the profiler service.
if(MessageEndpoint.IsValid())
{
MessageEndpoint->Send(FMessageEndpoint::MakeMessage<FProfilerServiceFileChunk>(FGuid(),FileChunk.Filename,FProfilerFileChunkHeader(0,0,0,EProfilerFileChunkType::FinalizeFile).AsArray()), Context->GetSender());
ProfilerFileTransferDelegate.Broadcast(FileChunk.Filename, ReceivedFileInfo->Progress, FileChunkHeader.FileSize);
}
// Delete the file writer.
delete ReceivedFileInfo->FileWriter;
ReceivedFileInfo->FileWriter = nullptr;
// Rename the stats file.
IFileManager::Get().Move(*ReceivedFileInfo->DestFilepath.Replace(StrTmp, TEXT("")), *ReceivedFileInfo->DestFilepath);
ActiveTransfers.Remove(FileChunk.Filename);
UE_LOG(LogProfilerClient, Log, TEXT("File service-client received successfully: %s"), *FileChunk.Filename);
}
else
{
ProfilerFileTransferDelegate.Broadcast(FileChunk.Filename, ReceivedFileInfo->Progress, FileChunkHeader.FileSize);
}
}
else
{
// This chunk is a bad chunk, so ask for resending it.
if(MessageEndpoint.IsValid())
{
MessageEndpoint->Send(FMessageEndpoint::MakeMessage<FProfilerServiceFileChunk>(FileChunk,FProfilerServiceFileChunk::FNullTag()), Context->GetSender());
UE_LOG(LogProfilerClient, Log, TEXT("Received a bad chunk of file, resending: %5i, %6u, %10u, %s"), FileChunk.HexData.Len(), ReceivedFileInfo->Progress, FileChunkHeader.FileSize, *FileChunk.Filename);
}
}
}
#endif
}
void FProfilerClientManager::HandleServicePingMessage(const FProfilerServicePing& Message, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& Context)
{
#if STATS
if (MessageEndpoint.IsValid())
{
TArray<FMessageAddress> Instances;
for (auto It = Connections.CreateConstIterator(); It; ++It)
{
Instances.Add(It.Value().ProfilerServiceAddress);
}
MessageEndpoint->Send(FMessageEndpoint::MakeMessage<FProfilerServicePong>(), Instances);
UE_LOG(LogProfilerClient, Verbose, TEXT("Ping GetSender: %s"), *Context->GetSender().ToString());
}
#endif
}
bool FProfilerClientManager::HandleTicker(float DeltaTime)
{
#if STATS
if (PendingInstances.Num() > 0 && FDateTime::Now() > LastPingTime + FTimespan::FromSeconds(DeltaTime))
{
TArray<FGuid> Instances;
Instances.Append(PendingInstances);
PendingInstances.Reset();
for (int32 i = 0; i < Instances.Num(); ++i)
{
Track(Instances[i]);
}
LastPingTime = FDateTime::Now();
}
#endif
return false;
}
bool FProfilerClientManager::HandleMessagesTicker(float DeltaTime)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_FProfilerClientManager_HandleMessagesTicker);
#if STATS
// MessageBus sends all data in out of order fashion.
// We buffer frame to make sure that all frames are received in the proper order.
const int32 NUM_BUFFERED_FRAMES = 15;
int32 NumConnectionsWithFrameData = 0;
for (auto It = Connections.CreateIterator(); It; ++It)
{
if (It.Value().ReceivedData.Num() >= NUM_BUFFERED_FRAMES)
{
NumConnectionsWithFrameData++;
}
}
if (NumConnectionsWithFrameData)
{
// Limit all processing while doing a live preview - otherwise the ping from a live connection may not be processed in time & the game will disconnect us.
// @todo all of this processing should move to a background thread or task, along with DecompressDataAndSendToGame
const double TimeLimitSeconds = bIsLivePreview ? 0.2 : 0.8;
const double MaxDurationSeconds = TimeLimitSeconds / (double)NumConnectionsWithFrameData;
for (auto It = Connections.CreateIterator(); It; ++It)
{
FServiceConnection& Connection = It.Value();
if (Connection.ReceivedData.Num() < NUM_BUFFERED_FRAMES)
{
break;
}
TArray<int64> Frames;
Connection.ReceivedData.GenerateKeyArray(Frames);
Frames.Sort();
uint64 StartTimeCycles = FPlatformTime::Cycles64();
for(int32 Index = 0; Index < Frames.Num(); Index++)
{
const int64 FrameNum = Frames[Index];
const TArray<uint8>* const Data = Connection.ReceivedData.FindChecked(FrameNum);
FStatsReadStream& Stream = Connection.Stream;
// Read all messages from the uncompressed buffer.
FMemoryReader MemoryReader(*Data, true);
while (MemoryReader.Tell() < MemoryReader.TotalSize())
{
// Read the message.
FStatMessage Message(Stream.ReadMessage(MemoryReader));
new (Connection.PendingStatMessagesMessages)FStatMessage(Message);
}
// Adds a new from from the pending messages, the pending messages will be removed after the call.
Connection.CurrentThreadState.ProcessMetaDataAndLeaveDataOnly(Connection.PendingStatMessagesMessages);
Connection.CurrentThreadState.AddFrameFromCondensedMessages(Connection.PendingStatMessagesMessages);
UE_LOG(LogProfilerClient, VeryVerbose, TEXT("Frame=%i/%i, FNamesIndexMap=%i, CurrentMetadataSize=%i"), FrameNum, Frames.Num(), Connection.Stream.FNamesIndexMap.Num(), Connection.CurrentThreadState.ShortNameToLongName.Num());
// create an old format data frame from the data
Connection.GenerateProfilerDataFrame();
// Fire a meta data update message
if (Connection.CurrentData.MetaDataUpdated)
{
ProfilerMetaDataUpdatedDelegate.Broadcast(Connection.InstanceId, Connection.StatMetaData);
}
// send the data out
ProfilerDataDelegate.Broadcast(Connection.InstanceId, Connection.CurrentData);
delete Data;
Connection.ReceivedData.Remove(FrameNum);
// see if we need to yield
double DurationSeconds = static_cast<double>(FPlatformTime::Cycles64() - StartTimeCycles) * FPlatformTime::GetSecondsPerCycle64();
if (DurationSeconds > MaxDurationSeconds)
{
UE_CLOG(Index < Frames.Num()-1, LogProfilerClient, Verbose, TEXT("Over time - %d/%d frames processed for connection %s"), Index, Frames.Num() - Index, *Connection.InstanceId.ToString() );
break;
}
}
}
}
// Remove any active transfer that timed out.
for(auto It = ActiveTransfers.CreateIterator(); It; ++It)
{
FReceivedFileInfo& ReceivedFileInfo = It.Value();
const FString& Filename = It.Key();
if(ReceivedFileInfo.IsTimedOut())
{
UE_LOG(LogProfilerClient, Log, TEXT("File service-client timed out, aborted: %s"), *Filename);
FailedTransfer.Add(Filename);
delete ReceivedFileInfo.FileWriter;
ReceivedFileInfo.FileWriter = nullptr;
IFileManager::Get().Delete(*ReceivedFileInfo.DestFilepath);
ProfilerFileTransferDelegate.Broadcast(Filename, -1, -1);
It.RemoveCurrent();
}
}
#endif
return true;
}
void FProfilerClientManager::HandleServicePreviewAckMessage(const FProfilerServicePreviewAck& Message, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& Context)
{
#if STATS
if (ActiveSessionId.IsValid() && Connections.Find(Message.InstanceId) != nullptr)
{
FServiceConnection& Connection = *Connections.Find(Message.InstanceId);
UE_LOG(LogProfilerClient, Verbose, TEXT("PreviewAck InstanceId: %s, GetSender: %s"), *Message.InstanceId.ToString(), *Context->GetSender().ToString());
}
#endif
}
void FProfilerClientManager::HandleProfilerServiceData2Message(const FProfilerServiceData2& Message, const TSharedRef<IMessageContext, ESPMode::ThreadSafe>& Context)
{
#if STATS
SCOPE_CYCLE_COUNTER(STAT_PC_HandleDataReceived);
if (ActiveSessionId.IsValid() && Connections.Find(Message.InstanceId) != nullptr)
{
LLM_SCOPE_BYNAME(TEXT("SessionProfiler"));
// Create a temporary profiler data and prepare all data.
FProfilerServiceData2* ToProcess = new FProfilerServiceData2(Message.InstanceId, Message.Frame, Message.HexData, Message.CompressedSize, Message.UncompressedSize);
// Decompression uses a task pipe
AsyncTaskPipe.Launch(UE_SOURCE_LOCATION, [this,ToProcess]()
{
DecompressDataAndSendToGame(ToProcess);
});
}
#endif
}
void FProfilerClientManager::DecompressDataAndSendToGame(FProfilerServiceData2* ToProcess)
{
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("FProfilerClientManager::DecompressDataAndSendToGame"), STAT_FProfilerClientManager_DecompressDataAndSendToGame, STATGROUP_Profiler);
// De-hex string into TArray<uint8>
TArray<uint8> CompressedData;
CompressedData.Reset(ToProcess->CompressedSize);
CompressedData.AddUninitialized(ToProcess->CompressedSize);
FString::ToHexBlob(ToProcess->HexData, CompressedData.GetData(), ToProcess->CompressedSize);
// Decompress data.
TArray<uint8> UncompressedData;
UncompressedData.Reset(ToProcess->UncompressedSize);
UncompressedData.AddUninitialized(ToProcess->UncompressedSize);
bool bResult = FCompression::UncompressMemory(NAME_Zlib, UncompressedData.GetData(), ToProcess->UncompressedSize, CompressedData.GetData(), ToProcess->CompressedSize);
check(bResult);
// Send to the game thread. Connections is not thread-safe, so we cannot add the data here.
TArray<uint8>* DateToGame = new TArray<uint8>(MoveTemp(UncompressedData));
FSimpleDelegateGraphTask::CreateAndDispatchWhenReady
(
FSimpleDelegateGraphTask::FDelegate::CreateRaw(this, &FProfilerClientManager::SendDataToGame, DateToGame, ToProcess->Frame, ToProcess->InstanceId),
TStatId(), nullptr, ENamedThreads::GameThread
);
delete ToProcess;
}
void FProfilerClientManager::SendDataToGame(TArray<uint8>* DataToGame, int64 Frame, const FGuid InstanceId)
{
if (ActiveSessionId.IsValid() && Connections.Find(InstanceId) != nullptr)
{
FServiceConnection& Connection = *Connections.Find(InstanceId);
// Add the message to the connections queue.
UE_LOG(LogProfilerClient, VeryVerbose, TEXT("Frame: %i, UncompressedSize: %i, InstanceId: %s"), Frame, DataToGame->Num(), *InstanceId.ToString());
Connection.ReceivedData.Add(Frame, DataToGame);
}
}
void FProfilerClientManager::SendProfilerDataFrameToGame(FProfilerDataFrame* NewData, FStatMetaData* MetaDataPtr, const FGuid InstanceId)
{
if (Connections.Find(InstanceId) != nullptr)
{
if (MetaDataPtr)
{
ProfilerMetaDataUpdatedDelegate.Broadcast(InstanceId, *MetaDataPtr);
delete MetaDataPtr;
MetaDataPtr = nullptr;
}
if (NewData)
{
ProfilerDataDelegate.Broadcast(InstanceId, *NewData);
delete NewData;
NewData = nullptr;
}
}
}
void FProfilerClientManager::Shutdown()
{
// Delete all active file writers and remove temporary files.
for (auto It = ActiveTransfers.CreateIterator(); It; ++It)
{
FReceivedFileInfo& ReceivedFileInfo = It.Value();
delete ReceivedFileInfo.FileWriter;
ReceivedFileInfo.FileWriter = nullptr;
IFileManager::Get().Delete(*ReceivedFileInfo.DestFilepath);
UE_LOG(LogProfilerClient, Log, TEXT("File service-client transfer aborted: %s"), *It.Key());
}
FTSTicker::GetCoreTicker().RemoveTicker(MessageDelegateHandle);
FTSTicker::GetCoreTicker().RemoveTicker(TickDelegateHandle);
}
void FProfilerClientManager::FinalizeLoading(const FGuid InstanceId)
{
if (Connections.Find(InstanceId) != nullptr)
{
ProfilerLoadCompletedDelegate.Broadcast(InstanceId);
LoadConnection = &Connections.FindChecked(InstanceId);
#if STATS
delete LoadConnection->StatsReader;
LoadConnection->StatsReader = nullptr;
#endif
LoadConnection = nullptr;
Connections.Remove(InstanceId);
RetryTime = 5.f;
}
}
void FProfilerClientManager::CancelLoading(const FGuid InstanceId)
{
if (Connections.Find(InstanceId) != nullptr)
{
ProfilerLoadCancelledDelegate.Broadcast(InstanceId);
LoadConnection = &Connections.FindChecked(InstanceId);
#if STATS
delete LoadConnection->StatsReader;
LoadConnection->StatsReader = nullptr;
#endif
LoadConnection = nullptr;
Connections.Remove(InstanceId);
}
}
#if STATS
int32 FServiceConnection::FindOrAddStat(const FStatNameAndInfo& StatNameAndInfo, uint32 StatType)
{
SCOPE_CYCLE_COUNTER(STAT_PC_FindOrAddStat);
const FName LongName = StatNameAndInfo.GetRawName();
int32* const StatIDPtr = LongNameToStatID.Find(LongName);
int32 StatID = StatIDPtr != nullptr ? *StatIDPtr : -1;
if (!StatIDPtr)
{
// meta data has been updated
CurrentData.MetaDataUpdated = true;
const FName StatName = StatNameAndInfo.GetShortName();
FName GroupName = StatNameAndInfo.GetGroupName();
const FString Description = StatNameAndInfo.GetDescription();
// do some special stats first
if (StatName == TEXT("STAT_FrameTime"))
{
StatID = LongNameToStatID.Add(LongName, 2);
}
else if (StatName == FStatConstants::NAME_ThreadRoot)
{
StatID = LongNameToStatID.Add(LongName, 1);
GroupName = TEXT("NoGroup");
}
else
{
StatID = LongNameToStatID.Add(LongName, LongNameToStatID.Num()+10);
}
check(StatID != -1);
// add a new stat description to the meta data
FStatDescription StatDescription;
StatDescription.ID = StatID;
StatDescription.Name = !Description.IsEmpty() ? Description : StatName.ToString();
if(StatDescription.Name.Contains(TEXT("STAT_")))
{
StatDescription.Name.RightChopInline(FString(TEXT("STAT_")).Len(), EAllowShrinking::No);
}
StatDescription.StatType = StatType;
if(GroupName == NAME_None && Stream.Header.Version == EStatMagicNoHeader::NO_VERSION)
{
// @todo Add more ways to group the stats.
const int32 Thread_Pos = StatDescription.Name.Find(TEXT("Thread_"));
const int32 _0Pos = StatDescription.Name.Find(TEXT("_0"));
const bool bIsThread = Thread_Pos != INDEX_NONE && _0Pos > Thread_Pos;
// Add a special group for all threads.
if(bIsThread)
{
GroupName = TEXT("Threads");
}
// Add a special group for all objects.
else
{
GroupName = TEXT("Objects");
}
}
int32* const GroupIDPtr = GroupNameArray.Find(GroupName);
int32 GroupID = GroupIDPtr != nullptr ? *GroupIDPtr : -1;
if(!GroupIDPtr)
{
// add a new group description to the meta data
GroupID = GroupNameArray.Add(GroupName, GroupNameArray.Num()+10);
check(GroupID != -1);
FStatGroupDescription GroupDescription;
GroupDescription.ID = GroupID;
GroupDescription.Name = GroupName.ToString();
GroupDescription.Name.RemoveFromStart(TEXT("STATGROUP_"));
// add to the meta data
StatMetaData.GroupDescriptions.Add(GroupDescription.ID, GroupDescription);
}
StatDescription.GroupID = GroupID;
StatMetaData.StatDescriptions.Add(StatDescription.ID, StatDescription);
}
// return the stat id
return StatID;
}
int32 FServiceConnection::FindOrAddThread(const FStatNameAndInfo& Thread)
{
SCOPE_CYCLE_COUNTER(STAT_PC_FindOrAddThread);
// The description of a thread group contains the thread id
const FString Desc = Thread.GetDescription();
const uint32 ThreadID = FStatsUtils::ParseThreadID(Desc);
const FName ShortName = Thread.GetShortName();
// add to the meta data
const int32 OldNum = StatMetaData.ThreadDescriptions.Num();
StatMetaData.ThreadDescriptions.Add(ThreadID, ShortName.ToString());
const int32 NewNum = StatMetaData.ThreadDescriptions.Num();
// meta data has been updated
CurrentData.MetaDataUpdated = CurrentData.MetaDataUpdated || OldNum != NewNum;
return ThreadID;
}
void FServiceConnection::GenerateAccumulators(TArray<FStatMessage>& Stats, TArray<FProfilerCountAccumulator>& CountAccumulators, TArray<FProfilerFloatAccumulator>& FloatAccumulators)
{
SCOPE_CYCLE_COUNTER(STAT_PC_GenerateAccumulator)
for (int32 Index = 0; Index < Stats.Num(); ++Index)
{
const FStatMessage& StatMessage = Stats[Index];
uint32 StatType = STATTYPE_Error;
if (StatMessage.NameAndInfo.GetField<EStatDataType>() == EStatDataType::ST_int64)
{
if (StatMessage.NameAndInfo.GetFlag(EStatMetaFlags::IsCycle))
{
StatType = STATTYPE_CycleCounter;
}
else if (StatMessage.NameAndInfo.GetFlag(EStatMetaFlags::IsMemory))
{
StatType = STATTYPE_MemoryCounter;
}
else
{
StatType = STATTYPE_AccumulatorDWORD;
}
}
else if (StatMessage.NameAndInfo.GetField<EStatDataType>() == EStatDataType::ST_double)
{
StatType = STATTYPE_AccumulatorFLOAT;
}
if (StatType != STATTYPE_Error)
{
const int32 StatId = FindOrAddStat(StatMessage.NameAndInfo, StatType);
if (StatMessage.NameAndInfo.GetField<EStatDataType>() == EStatDataType::ST_int64)
{
// add a count accumulator
FProfilerCountAccumulator Data;
Data.StatId = StatId;
Data.Value = static_cast<uint32>(StatMessage.GetValue_int64());
CountAccumulators.Add(Data);
}
else if (StatMessage.NameAndInfo.GetField<EStatDataType>() == EStatDataType::ST_double)
{
// add a float accumulator
FProfilerFloatAccumulator Data;
Data.StatId = StatId;
Data.Value = static_cast<float>(StatMessage.GetValue_double());
FloatAccumulators.Add(Data);
const FName StatName = StatMessage.NameAndInfo.GetRawName();
if (StatName == FStatConstants::RAW_SecondsPerCycle)
{
StatMetaData.SecondsPerCycle = StatMessage.GetValue_double();
}
}
}
}
}
void FServiceConnection::CreateGraphRecursively(const FRawStatStackNode* Root, FProfilerCycleGraph& Graph, uint32 InStartCycles)
{
Graph.FrameStart = InStartCycles;
Graph.StatId = FindOrAddStat(Root->Meta.NameAndInfo, STATTYPE_CycleCounter);
// add the data
if (Root->Meta.NameAndInfo.GetField<EStatDataType>() == EStatDataType::ST_int64)
{
if (Root->Meta.NameAndInfo.GetFlag(EStatMetaFlags::IsPackedCCAndDuration))
{
Graph.CallsPerFrame = FromPackedCallCountDuration_CallCount(Root->Meta.GetValue_int64());
Graph.Value = FromPackedCallCountDuration_Duration(Root->Meta.GetValue_int64());
}
else
{
Graph.CallsPerFrame = 1;
Graph.Value = static_cast<uint32>(Root->Meta.GetValue_int64());
}
}
uint32 ChildStartCycles = InStartCycles;
TArray<FRawStatStackNode*> ChildArray;
Root->Children.GenerateValueArray(ChildArray);
ChildArray.Sort(FStatDurationComparer<FRawStatStackNode>());
for(int32 Index = 0; Index < ChildArray.Num(); ++Index)
{
const FRawStatStackNode* ChildStat = ChildArray[Index];
// create the child graph
FProfilerCycleGraph ChildGraph;
ChildGraph.ThreadId = Graph.ThreadId;
CreateGraphRecursively(ChildStat, ChildGraph, ChildStartCycles);
// add to the graph
Graph.Children.Add(ChildGraph);
// update the start cycles
ChildStartCycles += ChildGraph.Value;
}
}
void FServiceConnection::GenerateCycleGraphs(const FRawStatStackNode& Root, TMap<uint32, FProfilerCycleGraph>& CycleGraphs)
{
SCOPE_CYCLE_COUNTER(STAT_PC_GenerateCycleGraph);
// Initialize the root stat.
FindOrAddStat(Root.Meta.NameAndInfo, STATTYPE_CycleCounter);
// get the cycle graph from each child of the stack root
TArray<FRawStatStackNode*> ChildArray;
Root.Children.GenerateValueArray(ChildArray);
for (int32 Index = 0; Index < ChildArray.Num(); ++Index)
{
FRawStatStackNode* ThreadNode = ChildArray[Index];
FProfilerCycleGraph Graph;
// determine the thread id
Graph.ThreadId = FindOrAddThread(ThreadNode->Meta.NameAndInfo);
// create the thread graph
CreateGraphRecursively(ThreadNode, Graph, 0);
// add to the map
CycleGraphs.Add(Graph.ThreadId, Graph);
}
}
void FServiceConnection::GenerateProfilerDataFrame()
{
SCOPE_CYCLE_COUNTER(STAT_PC_GenerateDataFrame);
FProfilerDataFrame& DataFrame = CurrentData;
DataFrame.Frame = static_cast<uint32>(CurrentThreadState.CurrentGameFrame);
DataFrame.FrameStart = 0.0;
DataFrame.CountAccumulators.Reset();
DataFrame.CycleGraphs.Reset();
DataFrame.FloatAccumulators.Reset();
DataFrame.MetaDataUpdated = false;
// get the stat stack root and the non frame stats
FRawStatStackNode Stack;
TArray<FStatMessage> NonFrameStats;
CurrentThreadState.UncondenseStackStats(CurrentThreadState.CurrentGameFrame, Stack, nullptr, &NonFrameStats);
// cycle graphs
GenerateCycleGraphs(Stack, DataFrame.CycleGraphs);
// accumulators
GenerateAccumulators(NonFrameStats, DataFrame.CountAccumulators, DataFrame.FloatAccumulators);
}
#endif