Files
UnrealEngine/Engine/Plugins/Online/OnlineFramework/Source/Qos/Private/QosStats.cpp
2025-05-18 13:04:45 +08:00

290 lines
10 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "QosStats.h"
#include "Interfaces/IAnalyticsProvider.h"
#define QOS_STATS_VERSION 2
#define DEBUG_QOS_STATS 0
// Events
const FString FQosDatacenterStats::QosStats_DatacenterEvent = TEXT("QosStats_DatacenterEvent");
// Common attribution
const FString FQosDatacenterStats::QosStats_SessionId = TEXT("SessionId");
const FString FQosDatacenterStats::QosStats_Version = TEXT("Version");
// Header stats
const FString FQosDatacenterStats::QosStats_Timestamp = TEXT("Timestamp");
const FString FQosDatacenterStats::QosStats_TotalTime = TEXT("TotalTime");
// Qos stats
const FString FQosDatacenterStats::QosStats_DeterminationType = TEXT("DeterminationType");
const FString FQosDatacenterStats::QosStats_NumRegions = TEXT("NumRegions");
const FString FQosDatacenterStats::QosStats_RegionDetails = TEXT("RegionDetailsv2");
const FString FQosDatacenterStats::QosStats_NumResults = TEXT("NumResults");
const FString FQosDatacenterStats::QosStats_NumSuccessCount = TEXT("NumSuccessCount");
const FString FQosDatacenterStats::QosStats_NetworkType = TEXT("NetworkType");
const FString FQosDatacenterStats::QosStats_BestRegionId = TEXT("BestRegionId");
const FString FQosDatacenterStats::QosStats_BestRegionPing = TEXT("BestRegionPing");
const FString FQosDatacenterStats::QosStats_BestEndpointId = TEXT("BestEndpointId");
const FString FQosDatacenterStats::QosStats_BestEndpointPing = TEXT("BestEndpointPing");
const FString FQosDatacenterStats::QosStats_ChosenRegionId = TEXT("ChosenbRegionId");
const FString FQosDatacenterStats::QosStats_ChosenSubRegionId = TEXT("ChosenSubRegionId");
const FString FQosDatacenterStats::QosStats_ChosenSubRegionPing = TEXT("ChosenSubRegionPing");
/**
* Debug output for the contents of a recorded stats event
*
* @param StatsEvent event type recorded
* @param Attributes attribution of the event
*/
inline void PrintEventAndAttributes(const FString& StatsEvent, const TArray<FAnalyticsEventAttribute>& Attributes)
{
#if DEBUG_QOS_STATS
UE_LOG(LogQos, Display, TEXT("Event: %s"), *StatsEvent);
for (int32 AttrIdx = 0; AttrIdx<Attributes.Num(); AttrIdx++)
{
const FAnalyticsEventAttribute& Attr = Attributes[AttrIdx];
UE_LOG(LogQos, Display, TEXT("\t%s : %s"), *Attr.GetName(), *Attr.GetValue());
}
#endif
}
FQosDatacenterStats::FQosDatacenterStats() :
StatsVersion(QOS_STATS_VERSION),
bAnalyticsInProgress(false)
{
}
void FQosDatacenterStats::StartTimer(FQosStats_Timer& Timer)
{
Timer.MSecs = FPlatformTime::Seconds();
Timer.bInProgress = true;
}
void FQosDatacenterStats::StopTimer(FQosStats_Timer& Timer)
{
if (Timer.bInProgress)
{
Timer.MSecs = (FPlatformTime::Seconds() - Timer.MSecs) * 1000;
Timer.bInProgress = false;
}
}
void FQosDatacenterStats::StartQosPass()
{
if (!bAnalyticsInProgress)
{
FDateTime UTCTime = FDateTime::UtcNow();
QosData.Timestamp = UTCTime.ToString();
StartTimer(QosData.SearchTime);
bAnalyticsInProgress = true;
}
}
void FQosDatacenterStats::RecordRegionInfo(const FDatacenterQosInstance& RegionInfo, int32 NumResults)
{
if (bAnalyticsInProgress)
{
FQosStats_RegionInfo& NewRegion = *new (QosData.Regions) FQosStats_RegionInfo();
NewRegion.RegionId = RegionInfo.Definition.Id;
NewRegion.ParentRegionId = RegionInfo.Definition.RegionId;
NewRegion.AvgPing = RegionInfo.AvgPingMs;
NewRegion.bUsable = RegionInfo.bUsable;
NewRegion.NumResults = NumResults;
}
}
void FQosDatacenterStats::RecordQosAttempt(const FString& Region, const FString& OwnerId, int32 PingInMs, bool bSuccess)
{
if (bAnalyticsInProgress)
{
QosData.NumTotalSearches++;
QosData.NumSuccessAttempts += bSuccess ? 1 : 0;
FQosStats_QosSearchResult& NewSearchResult = *new (QosData.SearchResults) FQosStats_QosSearchResult();
NewSearchResult.OwnerId = OwnerId;
NewSearchResult.PingInMs = PingInMs;
NewSearchResult.DatacenterId = Region;
NewSearchResult.bIsValid = true;
}
}
void FQosDatacenterStats::SetChosenSubRegion(FString&& SubRegion)
{
if (bAnalyticsInProgress)
{
QosData.RulesChosenSubRegion = MoveTemp(SubRegion);
}
}
void FQosDatacenterStats::SetChosenRegion(FString && Region)
{
if (bAnalyticsInProgress)
{
QosData.RulesChosenRegion = MoveTemp(Region);
}
}
void FQosDatacenterStats::EndQosPass(EDatacenterResultType Result)
{
if (bAnalyticsInProgress)
{
Finalize();
QosData.DeterminationType = Result;
}
}
void FQosDatacenterStats::Finalize()
{
StopTimer(QosData.SearchTime);
bAnalyticsInProgress = false;
}
void FQosDatacenterStats::Upload(TSharedPtr<IAnalyticsProvider>& AnalyticsProvider)
{
if (bAnalyticsInProgress)
{
Finalize();
}
// GUID representing the entire datacenter determination attempt
FGuid QosStatsGuid;
FPlatformMisc::CreateGuid(QosStatsGuid);
ParseQosResults(AnalyticsProvider, QosStatsGuid);
}
/**
* @EventName QosStats_DatacenterEvent
* @Trigger Attempt to determine a user datacenter from available QoS information
* @Type Client
* @EventParam SessionId string Guid of this attempt
* @EventParam Version integer Qos analytics version
* @EventParam Timestamp string Timestamp when this whole attempt started
* @EventParam TotalTime float Total time this complete attempt took, includes delay between all ping queries (ms)
* @EventParam DeterminationType how the region data was determined @see EDatacenterResultType
* @EventParam NumRegions integer Total number of regions considered or known at the time
* @EventParam NumResults integer Total number of results found for consideration
* @EventParam NumSuccessCount integer Total number of successful ping evaluations
* @EventParam NetworkType string type of network the client is connected to. (Unknown, None, AirplaneMode, Cell, Wifi, Ethernet) are possible values. Will be Unknown on PC and Switch.
* @EventParam BestRegionId string RegionId with best ping (that is usable, UNREACHABLE if none pass QoS)
* @EventParam BestRegionPing integer ping in the best RegionId (that is usable, 0 if BestRegionId is UNREACHABLE)
* @EventParam BestEndpointId string RegionId with best ping (regardless of usability, UNREACHABLE if none pass QoS)
* @EventParam BestEndpointPing integer ping in the best RegionId (regardless of usability, 0 if BestEndpointId is UNREACHABLE)
* @EventParam ChosenRegionId string RegionId chosen by RegionManager including bias, may differ form best (that is usable, UNREACHABLE if none pass QoS)
* @EventParam ChosenRegionPing integer ping in the chosen RegionId (that is usable, 0 if BestRegionId is UNREACHABLE)
* @EventParam RegionDetails json representation of ping details
* @Comments Analytics data for a complete qos datacenter determination attempt
*
* @Owner Josh.Markiewicz
*/
void FQosDatacenterStats::ParseQosResults(TSharedPtr<IAnalyticsProvider>& AnalyticsProvider, FGuid& SessionId)
{
TArray<FAnalyticsEventAttribute> QoSAttributes = MakeAnalyticsEventAttributeArray(
QosStats_SessionId, SessionId.ToString(),
QosStats_Version, StatsVersion,
QosStats_Timestamp, QosData.Timestamp,
QosStats_TotalTime, QosData.SearchTime.MSecs,
QosStats_DeterminationType, ToString(QosData.DeterminationType),
QosStats_NumRegions, QosData.Regions.Num(),
QosStats_NumResults, QosData.NumTotalSearches,
QosStats_NumSuccessCount, QosData.NumSuccessAttempts,
QosStats_NetworkType, FPlatformMisc::GetNetworkConnectionType()
);
{
FString BestRegionId(TEXT("Unknown"));
int32 BestPing = INT_MAX;
FString BestEndpointId(TEXT("Unknown"));
int32 BestEndpointPing = INT_MAX;
FString ChosenSubRegionId(TEXT("Unknown"));
int32 ChosenSubRegionPing = INT_MAX;
for (const FQosStats_RegionInfo& Region : QosData.Regions)
{
if (Region.AvgPing < BestPing)
{
if (Region.bUsable)
{
BestRegionId = Region.RegionId;
BestPing = Region.AvgPing;
}
BestEndpointId = Region.RegionId;
BestEndpointPing = Region.AvgPing;
}
if(Region.RegionId == QosData.RulesChosenSubRegion)
{
ChosenSubRegionId = Region.RegionId;
ChosenSubRegionPing = Region.AvgPing;
}
}
BestPing = FMath::Clamp(BestPing, 0, UNREACHABLE_PING);
ChosenSubRegionPing = FMath::Clamp(ChosenSubRegionPing, 0, UNREACHABLE_PING);
if (BestPing < UNREACHABLE_PING)
{
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestRegionId, BestRegionId));
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestRegionPing, BestPing));
}
else
{
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestRegionId, TEXT("UNREACHABLE")));
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestRegionPing, 0));
}
if (ChosenSubRegionPing < UNREACHABLE_PING)
{
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_ChosenSubRegionId, ChosenSubRegionId));
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_ChosenSubRegionPing, ChosenSubRegionPing));
}
else
{
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_ChosenSubRegionId, TEXT("UNREACHABLE")));
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_ChosenSubRegionPing, 0));
}
if(!QosData.RulesChosenRegion.IsEmpty())
{
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_ChosenRegionId, QosData.RulesChosenRegion));
}
BestEndpointPing = FMath::Clamp(BestEndpointPing, 0, UNREACHABLE_PING);
if (BestEndpointPing < UNREACHABLE_PING)
{
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestEndpointId, BestEndpointId));
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestEndpointPing, BestEndpointPing));
}
else
{
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestEndpointId, TEXT("UNREACHABLE")));
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_BestEndpointPing, 0));
}
}
{
FString StatsJson;
TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy< TCHAR > > > JsonWriter = TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy< TCHAR > >::Create(&StatsJson);
FJsonSerializerWriter<TCHAR, TCondensedJsonPrintPolicy< TCHAR > > Serializer(JsonWriter);
Serializer.StartArray();
for (FQosStats_RegionInfo& Region : QosData.Regions)
{
Region.Serialize(Serializer, false);
}
Serializer.EndArray();
JsonWriter->Close();
QoSAttributes.Add(FAnalyticsEventAttribute(QosStats_RegionDetails, FJsonFragment(MoveTemp(StatsJson))));
}
PrintEventAndAttributes(QosStats_DatacenterEvent, QoSAttributes);
if (AnalyticsProvider.IsValid())
{
AnalyticsProvider->RecordEvent(QosStats_DatacenterEvent, QoSAttributes);
}
}