// Copyright Epic Games, Inc. All Rights Reserved. #include "EditorTelemetry.h" #include "AnalyticsTracer.h" #include "AssetRegistry/AssetRegistryTelemetry.h" #include "CollectionManagerModule.h" #include "ContentBrowserModule.h" #include "ContentBrowserTelemetry.h" #include "CookOnTheSide/CookOnTheFlyServer.h" #include "DerivedDataCacheInterface.h" #include "DerivedDataCacheUsageStats.h" #include "Editor.h" #include "Editor/UnrealEdEngine.h" #include "Engine/AssetManager.h" #include "Experimental/ZenServerInterface.h" #include "FileHelpers.h" #include "HttpManager.h" #include "HttpModule.h" #include "Misc/FeedbackContext.h" #include "Modules/ModuleManager.h" #include "Policies/CondensedJsonPrintPolicy.h" #include "ProfilingDebugging/CookStats.h" #include "ShaderStats.h" #include "StudioTelemetry.h" #include "Subsystems/AssetEditorSubsystem.h" #include "TelemetryRouter.h" #include "UObject/ICookInfo.h" #include "UnrealEdGlobals.h" #include "Virtualization/VirtualizationSystem.h" namespace Private { const FName ContentBrowserModuleName = TEXT("ContentBrowser"); // Json writer subclass to allow us to avoid using a SharedPtr to write basic Json. typedef TCondensedJsonPrintPolicy FPrintPolicy; class FAnalyticsJsonWriter : public TJsonStringWriter { public: explicit FAnalyticsJsonWriter(FString* Out) : TJsonStringWriter(Out, 0) { } }; } const TCHAR* LexToString(ECollectionTelemetryAssetAddedWorkflow Enum) { switch(Enum) { case ECollectionTelemetryAssetAddedWorkflow::ContextMenu: return TEXT("ContextMenu"); case ECollectionTelemetryAssetAddedWorkflow::DragAndDrop: return TEXT("DragAndDrop"); default: return TEXT(""); } } const TCHAR* LexToString(ECollectionTelemetryAssetRemovedWorkflow Enum) { switch(Enum) { case ECollectionTelemetryAssetRemovedWorkflow::ContextMenu: return TEXT("ContextMenu"); default: return TEXT(""); } } template FString AnalyticsOptionalToStringOrNull(const TOptional& Opt) { return Opt.IsSet() ? AnalyticsConversionToString(Opt.GetValue()) : FString(TEXT("null")); } FEditorTelemetry& FEditorTelemetry::Get() { static FEditorTelemetry StudioTelemetryEditorInstance = FEditorTelemetry(); return StudioTelemetryEditorInstance; } void FEditorTelemetry::RecordEvent_Cooking(TArray Attributes) { #if ENABLE_COOK_STATS const int SchemaVersion = 3; Attributes.Emplace(TEXT("SchemaVersion"), SchemaVersion); TMap CookAttributes; // Sends each cook stat to the studio analytics system. auto GatherAnalyticsAttributes = [&CookAttributes](const FString& StatName, const TArray& StatAttributes) { for (const auto& Attr : StatAttributes) { const FString FormattedAttrName = (StatName + "_" + Attr.Key).Replace(TEXT("."), TEXT("_")); if (CookAttributes.Find(FormattedAttrName)==nullptr) { CookAttributes.Emplace(FormattedAttrName, Attr.Value.IsNumeric() ? FAnalyticsEventAttribute(FormattedAttrName, FCString::Atof(*Attr.Value)) : FAnalyticsEventAttribute(FormattedAttrName, Attr.Value)); } } }; // Now actually grab the stats FCookStatsManager::LogCookStats(GatherAnalyticsAttributes); // Add the values to the attributes for (TMap::TConstIterator it(CookAttributes); it; ++it) { Attributes.Emplace((*it).Value); } // Gather the DDC summary stats FDerivedDataCacheSummaryStats SummaryStats; GatherDerivedDataCacheSummaryStats(SummaryStats); // Append to the attributes for (const FDerivedDataCacheSummaryStat& Stat : SummaryStats.Stats) { FString AttributeName = TEXT("DDC_Summary") + Stat.Key.Replace(TEXT("."), TEXT("_")); if (Stat.Value.IsNumeric()) { Attributes.Emplace(AttributeName, FCString::Atof(*Stat.Value)); } else { Attributes.Emplace(AttributeName, Stat.Value); } } #if UE_WITH_ZEN // Gather Zen analytics if (UE::Zen::IsDefaultServicePresent()) { UE::Zen::GetDefaultServiceInstance().GatherAnalytics(Attributes); } #endif if (UE::Virtualization::IVirtualizationSystem::Get().IsEnabled()) { // Gather Virtualization analytics UE::Virtualization::IVirtualizationSystem::Get().GatherAnalytics(Attributes); } FShaderStatsFunctions::GatherShaderAnalytics(Attributes); FStudioTelemetry::Get().RecordEvent(TEXT("Core.Cooking"), Attributes); #endif } void FEditorTelemetry::RecordEvent_Loading(const FString& Context, double LoadingSeconds, TArray Attributes ) { const int SchemaVersion = 4; Attributes.Emplace(TEXT("SchemaVersion"), SchemaVersion); Attributes.Emplace(TEXT("Context"), Context); Attributes.Emplace(TEXT("LoadingName"), Context); Attributes.Emplace(TEXT("LoadingSeconds"), LoadingSeconds); #if ENABLE_COOK_STATS #if UE_WITH_ZEN // Gather Zen analytics if (UE::Zen::IsDefaultServicePresent()) { UE::Zen::GetDefaultServiceInstance().GatherAnalytics(Attributes); } #endif if (UE::Virtualization::IVirtualizationSystem::Get().IsEnabled()) { // Gather Virtualization analytics UE::Virtualization::IVirtualizationSystem::Get().GatherAnalytics(Attributes); } // Gather the DDC summary stats FDerivedDataCacheSummaryStats SummaryStats; GatherDerivedDataCacheSummaryStats(SummaryStats); // Append to the attributes for (const FDerivedDataCacheSummaryStat& Stat : SummaryStats.Stats) { FString AttributeName = TEXT("DDC_Summary_") + Stat.Key.Replace(TEXT("."), TEXT("_")); if (Stat.Value.IsNumeric()) { Attributes.Emplace(AttributeName, FCString::Atof(*Stat.Value)); } else { Attributes.Emplace(AttributeName, Stat.Value); } } #endif FStudioTelemetry::Get().RecordEvent(TEXT("Core.Loading"), Attributes); } void FEditorTelemetry::RecordEvent_DDCResource(const FString& Context, TArray Attributes) { #if ENABLE_COOK_STATS // Gather the latest resource stats TArray ResourceStats; GatherDerivedDataCacheResourceStats(ResourceStats); const int SchemaVersion = 4; Attributes.Emplace(TEXT("SchemaVersion"), SchemaVersion); Attributes.Emplace(TEXT("Context"), Context); // Send a resource event per asset type for (const FDerivedDataCacheResourceStat& Stat : ResourceStats) { const double TotalTimeSec = Stat.BuildTimeSec + Stat.LoadTimeSec; const int64 TotalSizeMB = Stat.BuildSizeMB + Stat.LoadSizeMB; if (Stat.AssetType.IsEmpty() || Stat.TotalCount==0) { // Empty asset type or nothing was built or loaded for this type continue; } TArray EventAttributes = Attributes; EventAttributes.Emplace(TEXT("AssetType"), Stat.AssetType); EventAttributes.Emplace(TEXT("Load_Count"), Stat.LoadCount); EventAttributes.Emplace(TEXT("Load_TimeSec"), Stat.LoadTimeSec); EventAttributes.Emplace(TEXT("Load_SizeMB"), Stat.LoadSizeMB); EventAttributes.Emplace(TEXT("Build_Count"), Stat.BuildCount); EventAttributes.Emplace(TEXT("Build_TimeSec"), Stat.BuildTimeSec); EventAttributes.Emplace(TEXT("Build_SizeMB"), Stat.BuildSizeMB); EventAttributes.Emplace(TEXT("Total_Count"), Stat.TotalCount); EventAttributes.Emplace(TEXT("Total_TimeSec"), TotalTimeSec); EventAttributes.Emplace(TEXT("Total_SizeMB"), TotalSizeMB); EventAttributes.Emplace(TEXT("Efficiency"), Stat.Efficiency); EventAttributes.Emplace(TEXT("Thread_TimeSec"), Stat.GameThreadTimeSec); FStudioTelemetry::Get().RecordEvent(TEXT("Core.DDC.Resource"), EventAttributes); } #endif } void FEditorTelemetry::RecordEvent_DDCSummary(const FString& Context, TArray Attributes) { #if ENABLE_COOK_STATS const int SchemaVersion = 4; Attributes.Emplace(TEXT("SchemaVersion"), SchemaVersion); Attributes.Emplace(TEXT("Context"), Context); // Gather the summary stats FDerivedDataCacheSummaryStats SummaryStats; GatherDerivedDataCacheSummaryStats(SummaryStats); // Append to the attributes for (const FDerivedDataCacheSummaryStat& Stat : SummaryStats.Stats) { FString AttributeName = Stat.Key.Replace(TEXT("."), TEXT("_")); if (Stat.Value.IsNumeric()) { Attributes.Emplace(AttributeName, FCString::Atof(*Stat.Value)); } else { Attributes.Emplace(AttributeName, Stat.Value); } } FStudioTelemetry::Get().RecordEvent(TEXT("Core.DDC.Summary"), Attributes); #endif } void FEditorTelemetry::RecordEvent_Zen(const FString& Context, TArray Attributes) { #if UE_WITH_ZEN // Gather Zen analytics if (UE::Zen::IsDefaultServicePresent()) { const int SchemaVersion = 2; Attributes.Emplace(TEXT("SchemaVersion"), SchemaVersion); Attributes.Emplace(TEXT("Context"), Context); UE::Zen::GetDefaultServiceInstance().GatherAnalytics(Attributes); FStudioTelemetry::Get().RecordEvent(TEXT("Core.Zen"), Attributes); } #endif } void FEditorTelemetry::RecordEvent_VirtualAssets(const FString& Context, TArray Attributes) { if (UE::Virtualization::IVirtualizationSystem::Get().IsEnabled()) { const int SchemaVersion = 2; Attributes.Emplace(TEXT("SchemaVersion"), SchemaVersion); Attributes.Emplace(TEXT("Context"), Context); // Gather Virtualization analytics UE::Virtualization::IVirtualizationSystem::Get().GatherAnalytics(Attributes); FStudioTelemetry::Get().RecordEvent(TEXT("Core.VirtualAssets"), Attributes); } } void FEditorTelemetry::RecordEvent_MemoryLLM(const FString& Context, TArray Attributes) { #if ENABLE_LOW_LEVEL_MEM_TRACKER if (FStudioTelemetry::Get().IsSessionRunning()) { auto RecordLLMMemoryEvent = [&Attributes](const FString& Context, const FString& TagSet, TMap& LLMTrackedMemoryMap) { for (TMap::TConstIterator It(LLMTrackedMemoryMap); It; ++It) { const int SchemaVersion = 2; TArray EventAttributes = Attributes; EventAttributes.Emplace(TEXT("SchemaVersion"), SchemaVersion); EventAttributes.Emplace(TEXT("Context"), Context); EventAttributes.Emplace(TEXT("TagSet"), TagSet); EventAttributes.Emplace(TEXT("Name"), It->Key); EventAttributes.Emplace(TEXT("Size"), It->Value); FStudioTelemetry::Get().RecordEvent("Core.Memory.LLM", EventAttributes); } }; // None TagSet TMap LLMTrackedNoneMemory; FLowLevelMemTracker::Get().GetTrackedTagsNamesWithAmount(LLMTrackedNoneMemory, ELLMTracker::Default, ELLMTagSet::None); RecordLLMMemoryEvent(Context, TEXT("None"), LLMTrackedNoneMemory); // AssetClasses TagSet TMap LLMTrackedAssetClassesMemory; FLowLevelMemTracker::Get().GetTrackedTagsNamesWithAmount(LLMTrackedAssetClassesMemory, ELLMTracker::Default, ELLMTagSet::AssetClasses); RecordLLMMemoryEvent(Context, TEXT("AssetClasses"), LLMTrackedAssetClassesMemory); // Asset TagSet TMap LLMTrackedAssetMemory; FLowLevelMemTracker::Get().GetTrackedTagsNamesWithAmount(LLMTrackedAssetMemory, ELLMTracker::Default, ELLMTagSet::Assets); RecordLLMMemoryEvent(Context, TEXT("Assets"), LLMTrackedAssetMemory); } #endif } void FEditorTelemetry::RecordEvent_CoreSystems(const FString& Context, TArray Attributes) { FEditorTelemetry::RecordEvent_DDCResource(Context, Attributes); FEditorTelemetry::RecordEvent_DDCSummary(Context, Attributes); FEditorTelemetry::RecordEvent_Zen(Context, Attributes); FEditorTelemetry::RecordEvent_VirtualAssets(Context, Attributes); FEditorTelemetry::RecordEvent_MemoryLLM(Context, Attributes); } void FEditorTelemetry::RegisterCollectionWorkflowDelegates(FTelemetryRouter& Router) { Router.OnTelemetry([](const FAssetAddedToCollectionTelemetryEvent& Event) { const int SchemaVersion = 1; FStudioTelemetry::Get().RecordEvent(TEXT("Editor.Collections.AssetsAdded"), { { TEXT("SchemaVersion"), SchemaVersion}, { TEXT("DurationSec"), Event.DurationSec}, { TEXT("ObjectCount"), Event.NumAdded}, { TEXT("Workflow"), Event.Workflow}, { TEXT("CollectionShareType"), ECollectionShareType::ToString(Event.CollectionShareType)}, }); }); Router.OnTelemetry([](const FAssetRemovedFromCollectionTelemetryEvent& Event) { const int SchemaVersion = 1; FStudioTelemetry::Get().RecordEvent(TEXT("Editor.Collections.AssetsRemoved"), { { TEXT("SchemaVersion"), SchemaVersion}, { TEXT("DurationSec"), Event.DurationSec}, { TEXT("ObjectCount"), Event.NumRemoved}, { TEXT("Workflow"), Event.Workflow}, { TEXT("CollectionShareType"), ECollectionShareType::ToString(Event.CollectionShareType) }, }); }); Router.OnTelemetry([](const FCollectionCreatedTelemetryEvent& Event) { const int SchemaVersion = 1; FStudioTelemetry::Get().RecordEvent(TEXT("Editor.Collections.CollectionCreated"), { { TEXT("SchemaVersion"), SchemaVersion}, { TEXT("DurationSec"), Event.DurationSec}, { TEXT("CollectionShareType"), ECollectionShareType::ToString(Event.CollectionShareType)}, }); }); Router.OnTelemetry([](const FCollectionsDeletedTelemetryEvent& Event) { const int SchemaVersion = 1; FStudioTelemetry::Get().RecordEvent(TEXT("Editor.Collections.CollectionDeleted"), { { TEXT("SchemaVersion"), SchemaVersion}, { TEXT("DurationSec"), Event.DurationSec}, { TEXT("ObjectCount"), Event.CollectionsDeleted}, }); }); } extern ENGINE_API float GAverageFPS; void FEditorTelemetry::HitchSamplerCallback() { // Only sample framerate when we have focus if (FApp::HasFocus()) { // Sample a rolling average of FPS HitchAvergageFPS = (HitchAvergageFPS * HitchSampleCount + GAverageFPS) / (double)(HitchSampleCount + 1); HitchSampleCount++; } } void FEditorTelemetry::HeartbeatCallback() { if (HitchSampleCount>0) { // Hitching is when FPS is below our threshold const bool IsHitching = HitchAvergageFPS < MinFPSForHitching; static uint32 HitchCount = 0; HitchCount += IsHitching ? 1 : 0; if (IsHitching == false && HitchingSpan.IsValid() == true) { // No longer hitching and we have started a hitch span TArray Attributes; const double ElapsedTime = HitchingSpan->GetElapsedTime(); Attributes.Emplace(FAnalyticsEventAttribute(TEXT("Hitch_Count"), HitchCount)); Attributes.Emplace(FAnalyticsEventAttribute(TEXT("Hitch_HitchesPerSecond"), ElapsedTime>0? (float)HitchCount / ElapsedTime : 0.0)); Attributes.Emplace(FAnalyticsEventAttribute(TEXT("Hitch_AverageFPS"), HitchAvergageFPS)); Attributes.Emplace(TEXT("MapName"), EditorMapName); Attributes.Emplace(TEXT("PIE_MapName"), PIEMapName); // End the hitch Span FStudioTelemetry::Get().EndSpan(HitchingSpan, Attributes); // Record the hitch event FStudioTelemetry::Get().RecordEvent(TEXT("Core.Hitch"), Attributes); // Record core systems events for the hitch RecordEvent_CoreSystems(TEXT("Hitch")); // No longer need the hitch span for now so reset it HitchingSpan.Reset(); } else if (IsHitching == true && HitchingSpan.IsValid() == false) { // We are hitching and we have not started a hitch span HitchingSpan = FStudioTelemetry::Get().StartSpan(HitchingSpanName); HitchCount = 1; } // Reset the hitch sampler HitchSampleCount = 0; HitchAvergageFPS = 0; } } void FEditorTelemetry::StartSession() { if (FStudioTelemetry::Get().IsSessionRunning() == false) { return; } SessionStartTime = FPlatformTime::Seconds(); // Install Editor Only Mode callbacks. Do not record these for Editor Commandlet runs if (GIsEditor==true && IsRunningCommandlet()==false) { // Start Editor and Editor Boot span. Note : this will only start when the plugin is loaded and as such will miss any activity that runs beforehand EditorSpan = FStudioTelemetry::Get().StartSpan(EditorSpanName); EditorBootSpan = FStudioTelemetry::Get().StartSpan(EditorBootSpanName, EditorSpan); EditorMapName = TEXT("None"); TArray Attributes; Attributes.Emplace(FAnalyticsEventAttribute(TEXT("MapName"), EditorMapName)); EditorBootSpan->AddAttributes(Attributes); FEditorDelegates::OnEditorBoot.AddLambda([this](double) { TArray Attributes; Attributes.Emplace(TEXT("PluginCount"), TotalPluginCount); EditorBootSpan->AddAttributes(Attributes); FStudioTelemetry::Get().EndSpan(EditorBootSpan); // Callback is received when the editor has booted but has not been initialized FEditorTelemetry::RecordEvent_Loading(TEXT("Editor.Boot"), EditorBootSpan->GetDuration(), EditorBootSpan->GetAttributes()); FEditorTelemetry::RecordEvent_CoreSystems(TEXT("Editor.Boot"), EditorBootSpan->GetAttributes()); EditorInitilizeSpan = FStudioTelemetry::Get().StartSpan(EditorInitilizeSpanName, EditorSpan); }); FEditorDelegates::OnEditorInitialized.AddLambda([this](double TimeToInitializeEditor) { TimeToBootEditor = TimeToInitializeEditor; // Editor has initialized TArray Attributes; Attributes.Emplace(TEXT("MapName"), EditorMapName); Attributes.Emplace(TEXT("PluginCount"), TotalPluginCount); EditorInitilizeSpan->AddAttributes(Attributes); // Editor has finished initializing so start the Editor Interact span FStudioTelemetry::Get().EndSpan(EditorInitilizeSpan); FEditorTelemetry::RecordEvent_Loading(TEXT("Editor.Initialize"), EditorInitilizeSpan->GetDuration(), EditorInitilizeSpan->GetAttributes()); FEditorTelemetry::RecordEvent_CoreSystems(TEXT("Editor.Initialize"), EditorInitilizeSpan->GetAttributes()); FEditorTelemetry::RecordEvent_Loading(TEXT("TimeToEdit"), TimeToInitializeEditor, EditorInitilizeSpan->GetAttributes()); EditorInteractSpan = FStudioTelemetry::Get().StartSpan(EditorInteractSpanName, EditorSpan); // Install callbacks for Open Asset Dialogue GEditor->GetEditorSubsystem()->OnAssetEditorRequestedOpen().AddLambda([this](UObject* Asset) { AssetOpenStartTime = FPlatformTime::Seconds(); FStudioTelemetry::Get().StartSpan(OpenAssetEditorSpan); }); GEditor->GetEditorSubsystem()->OnAssetOpenedInEditor().AddLambda([this](UObject* Asset, IAssetEditorInstance*) { TArray Attributes; Attributes.Emplace(TEXT("MapName"), EditorMapName); if (Asset != nullptr) { Attributes.Emplace(TEXT("AssetPath"), Asset->GetFullName()); Attributes.Emplace(TEXT("AssetClass"), Asset->GetClass()->GetName()); } FStudioTelemetry::Get().EndSpan(OpenAssetEditorSpan, Attributes); }); // Setup a timer for a Heartbeat callback FTimerDelegate HeartbeatDelegate; HeartbeatDelegate.BindRaw(this, &FEditorTelemetry::HeartbeatCallback); GEditor->GetTimerManager()->SetTimer(TelemetryHeartbeatTimerHandle, HeartbeatDelegate, HeartbeatIntervalSeconds, true); // Setup the timer for the Hitch Detector callback FTimerDelegate HitchSamplerDelegate; HitchSamplerDelegate.BindRaw(this, &FEditorTelemetry::HitchSamplerCallback); GEditor->GetTimerManager()->SetTimer(TelemetryHitchSamplerTimerHandle, HitchSamplerDelegate, HitchSamplerIntervalSeconds, true); }); // Install PIE Mode callbacks FEditorDelegates::StartPIE.AddLambda([this](bool) { // PIE mode has been started. The user has pressed the Start PIE button. // Finish the Editor span FStudioTelemetry::Get().EndSpan(EditorSpan); EditorSessionCount++; // Start PIE span PIESpan = FStudioTelemetry::Get().StartSpan(PIESpanName); // Append the PIE transition count to the PIE name PIEStartupSpan = FStudioTelemetry::Get().StartSpan(PIESessionCount == 0 ? PIEStartupSpanName : FName(*FString::Printf(TEXT("%s%d"), *PIEStartupSpanName.ToString(), PIESessionCount)), PIESpan); TArray Attributes; Attributes.Emplace(TEXT("MapName"), EditorMapName); PIESpan->AddAttributes(Attributes); PIEStartupSpan->AddAttributes(Attributes); }); FEditorDelegates::PreBeginPIE.AddLambda([this](bool) { PIEPreBeginSpan = FStudioTelemetry::Get().StartSpan(PIESessionCount == 0 ? PIEPreBeginSpanName : FName(*FString::Printf(TEXT("%s%d"), *PIEPreBeginSpanName.ToString(), PIESessionCount)), PIESpan); PIEPreBeginSpan->AddAttributes(PIESpan->GetAttributes()); }); FEditorDelegates::BeginPIE.AddLambda([this](bool) { FStudioTelemetry::Get().EndSpan(PIEPreBeginSpan); }); FWorldDelegates::OnPIEMapCreated.AddLambda([this](UGameInstance* GameInstance) { // A new PIE map was created PIELoadMapSpan = FStudioTelemetry::Get().StartSpan(PIELoadMapSpanName, PIEStartupSpan); }); FWorldDelegates::OnPIEMapReady.AddLambda([this](UGameInstance* GameInstance) { // PIE map is now loaded and ready to use PIEMapName = FPaths::GetBaseFilename(GameInstance->PIEMapName); TArray Attributes; Attributes.Emplace(TEXT("MapName"), EditorMapName); Attributes.Emplace(TEXT("PIE_MapName"), PIEMapName); PIELoadMapSpan->AddAttributes(Attributes); FStudioTelemetry::Get().EndSpan(PIELoadMapSpan); FEditorTelemetry::RecordEvent_Loading(TEXT("PIE.LoadMap"), PIELoadMapSpan->GetDuration(), PIELoadMapSpan->GetAttributes()); FEditorTelemetry::RecordEvent_CoreSystems(TEXT("PIE.LoadMap"), PIELoadMapSpan->GetAttributes()); }); FWorldDelegates::OnPIEReady.AddLambda([this](UGameInstance* GameInstance) { if (PIESpan.IsValid()) { if (PIEStartupSpan.IsValid()) { // PIE is now ready for user interaction // Keep track of the PIE transition counts TArray Attributes; Attributes.Emplace(TEXT("PIE_TransitionCount"), PIESessionCount); PIESpan->AddAttributes(Attributes); PIEStartupSpan->AddAttributes(Attributes); FStudioTelemetry::Get().EndSpan(PIEStartupSpan); // Record the PIE startup FEditorTelemetry::RecordEvent_Loading(TEXT("PIE.Startup"), PIEStartupSpan->GetDuration(), PIEStartupSpan->GetAttributes()); FEditorTelemetry::RecordEvent_CoreSystems(TEXT("PIE.Startup"), PIEStartupSpan->GetAttributes()); // Record the time from start PIE to PIE if (PIESessionCount == 0) { const double TimeInEditor = EditorLoadMapSpan.IsValid() ? EditorLoadMapSpan->GetDuration() : 0.0; const double TimeToStartPIE = PIEStartupSpan->GetDuration(); const double TimeToBootToPIE = TimeToBootEditor + TimeInEditor + TimeToStartPIE; // Record the absolute time from editor boot to PIE FEditorTelemetry::RecordEvent_Loading(TEXT("TimeToPIE"), TimeToBootToPIE, PIEStartupSpan->GetAttributes()); } } PIEInteractSpan = FStudioTelemetry::Get().StartSpan(PIESessionCount == 0 ? PIEInteractSpanName : FName(*FString::Printf(TEXT("%s%d"), *PIEInteractSpanName.ToString(), PIESessionCount)), PIESpan); PIEInteractSpan->AddAttributes(PIESpan->GetAttributes()); } }); FEditorDelegates::EndPIE.AddLambda([this](bool) { if (PIESpan.IsValid()) { // PIE is ending so no longer interactive FStudioTelemetry::Get().EndSpan(PIEInteractSpan); PIEShutdownSpan = FStudioTelemetry::Get().StartSpan(PIESessionCount == 0 ? PIEShutdownSpanName : FName(*FString::Printf(TEXT("%s%d"), *PIEShutdownSpanName.ToString(), PIESessionCount)), PIESpan); PIEShutdownSpan->AddAttributes(PIESpan->GetAttributes()); } }); FEditorDelegates::ShutdownPIE.AddLambda([this](bool) { if (PIESpan.IsValid()) { // PIE has shutdown, ie. the user has pressed the Stop PIE button, and we are going back to interactive Editor mode FStudioTelemetry::Get().EndSpan(PIEShutdownSpan); FEditorTelemetry::RecordEvent_Loading(TEXT("PIE.Shutdown"), PIEShutdownSpan->GetDuration(), PIEShutdownSpan->GetAttributes()); FEditorTelemetry::RecordEvent_CoreSystems(TEXT("PIE.Shutdown"), PIEShutdownSpan->GetAttributes()); FStudioTelemetry::Get().EndSpan(PIESpan); } PIESessionCount++; TArray Attributes; Attributes.Emplace(TEXT("MapName"), EditorMapName); // Restart the Editor span EditorSpan = FStudioTelemetry::Get().StartSpan(EditorSpanName, Attributes); EditorInteractSpan = FStudioTelemetry::Get().StartSpan(FName(*FString::Printf(TEXT("%s%d"), *EditorInteractSpanName.ToString(), EditorSessionCount)), EditorSpan); }); FEditorDelegates::OnMapLoad.AddLambda([this](const FString& MapName, FCanLoadMap& OutCanLoadMap) { if (MapName.Len() > 0) { // The Editor loads a new map EditorLoadMapSpan = FStudioTelemetry::Get().StartSpan(EditorLoadMapSpanName, EditorSpan); } }); FEditorDelegates::OnMapOpened.AddLambda([this](const FString& MapName, bool Unused) { if (EditorLoadMapSpan.IsValid()) { // The new editor map was actually opened EditorMapName = FPaths::GetBaseFilename(MapName); TArray Attributes; Attributes.Emplace(TEXT("MapName"), EditorMapName); EditorSpan->AddAttributes(Attributes); EditorLoadMapSpan->AddAttributes(Attributes); FStudioTelemetry::Get().EndSpan(EditorLoadMapSpan); FEditorTelemetry::RecordEvent_Loading(TEXT("Editor.LoadMap"), EditorLoadMapSpan->GetDuration(), EditorLoadMapSpan->GetAttributes()); FEditorTelemetry::RecordEvent_CoreSystems(TEXT("Editor.LoadMap"), EditorLoadMapSpan->GetAttributes()); EditorLoadMapSpan.Reset(); } }); } // Install any plugin load/unload callbacks FModuleManager::Get().OnModulesChanged().AddLambda([this](FName ModuleName, EModuleChangeReason ChangeReason) { switch (ChangeReason) { default: { break; } case EModuleChangeReason::ModuleLoaded: { TotalPluginCount++; // Hook into Asset Registry Scan callbacks as as soon as it is loaded if (ModuleName == TEXT("AssetRegistry")) { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); AssetRegistryModule.Get().OnScanStarted().AddLambda([this]() { if (AssetRegistryScanCount == 0) { // Start the Asset Registry Scan span AssetRegistryScanSpan = FStudioTelemetry::Get().StartSpan(AssetRegistryScanSpanName, EditorSpan); } AssetRegistryScanCount++; }); AssetRegistryModule.Get().OnScanEnded().AddLambda([this]() { AssetRegistryScanCount--; if (AssetRegistryScanCount == 0) { // End the Asset Registry Scan span TArray Attributes; Attributes.Emplace(TEXT("MapName"), EditorMapName); FStudioTelemetry::Get().EndSpan(AssetRegistryScanSpan, Attributes); } }); } break; } case EModuleChangeReason::ModuleUnloaded: { TotalPluginCount--; break; } } }); // Set up Slow Task callbacks ensureMsgf(GWarn, TEXT("GWarn was not valid")); if (GWarn != nullptr) { // Start the SlowTask span GWarn->OnStartSlowTaskWithGuid().AddLambda([this](FGuid TaskGuid, const FText& TaskName) { // Slow tasks can possibly be started from multiple threads, so we need to protect the registered span table FScopeLock ScopeLock(&TaskSpanCriticalSection); TSharedPtr* SpanPtr = TaskSpans.Find(TaskGuid); // Only one task with this Guid is running asynchronously is supported at this time. if (SpanPtr == nullptr) { TArray Attributes; Attributes.Emplace(TEXT("MapName"), EditorMapName); Attributes.Emplace(TEXT("TaskName"), TaskName.ToString()); // Create and start a new slow task span TSharedPtr SlowTaskSpan = FStudioTelemetry::Get().StartSpan(TEXT("SlowTask"), Attributes); // Store this SlowTask span so we can find it when it finishes TaskSpans.Add(TaskGuid, SlowTaskSpan); } TRACE_BEGIN_REGION(*TaskName.ToString()); }); // End the SlowTask span GWarn->OnFinalizeSlowTaskWithGuid().AddLambda([this](FGuid TaskGuid, const FText& TaskName) { TRACE_END_REGION(*TaskName.ToString()); // Slow tasks can possibly be finalized from multiple threads, so we need to protect the registered span table FScopeLock ScopeLock(&TaskSpanCriticalSection); // Find the task we stored off when we started this task TSharedPtr* SpanPtr = TaskSpans.Find(TaskGuid); if (SpanPtr != nullptr) { TSharedPtr SlowTaskSpan = *SpanPtr; FStudioTelemetry::Get().EndSpan(SlowTaskSpan); // Remove the SlowTask span from the registry TaskSpans.Remove(TaskGuid); } }); } // Install Cooking Callbacks if (GUnrealEd != nullptr && GUnrealEd->CookServer != nullptr) { UE::Cook::FDelegates::CookFinished.AddLambda([this](UE::Cook::ICookInfo& CookInfo) { if (CookInfo.GetCookType() != UE::Cook::ECookType::ByTheBook) { return; } TArray Attributes; Attributes.Emplace(TEXT("MapName"), EditorMapName); FEditorTelemetry::RecordEvent_Cooking(Attributes); FEditorTelemetry::RecordEvent_CoreSystems(TEXT("Cooking"), Attributes); }); } UE::Cook::FDelegates::CookStarted.AddLambda([this](UE::Cook::ICookInfo& CookInfo) { if (CookInfo.GetCookType() != UE::Cook::ECookType::ByTheBook) { return; } // Begin the cooking span CookingSpan = FStudioTelemetry::Get().StartSpan(TEXT("Cooking")); TArray Attributes; Attributes.Emplace(TEXT("MapName"), EditorMapName); CookingSpan->AddAttributes(Attributes); }); UE::Cook::FDelegates::CookFinished.AddLambda([this](UE::Cook::ICookInfo& CookInfo) { if (CookInfo.GetCookType() != UE::Cook::ECookType::ByTheBook) { return; } // End the cooking span // Suppress sending telemetry from CookWorkers for now. uint32 MultiprocessId = 0; FParse::Value(FCommandLine::Get(), TEXT("-MultiprocessId="), MultiprocessId); if (MultiprocessId != 0) { return; } FEditorTelemetry::RecordEvent_Cooking(CookingSpan->GetAttributes()); FEditorTelemetry::RecordEvent_CoreSystems(TEXT("Cooking"), CookingSpan->GetAttributes()); FStudioTelemetry::Get().EndSpan(CookingSpan); }); // Install Content Browser callbacks FContentBrowserModule* ContentBrowserModule = FModuleManager::GetModulePtr( TEXT("ContentBrowser") ); FTelemetryRouter& Router = FTelemetryRouter::Get(); { using namespace UE::Telemetry::ContentBrowser; Router.OnTelemetry([this](const FBackendFilterTelemetry& Data) { FString DataFilterText = LexToString(FJsonNull{}); if (Data.DataFilter) { Private::FAnalyticsJsonWriter J(&DataFilterText); J.WriteObjectStart(); J.WriteValue("RecursivePaths", Data.DataFilter->bRecursivePaths); J.WriteValue("ItemTypeFilter", UEnum::GetValueOrBitfieldAsString(Data.DataFilter->ItemTypeFilter)); J.WriteValue("ItemCategoryFilter", UEnum::GetValueOrBitfieldAsString(Data.DataFilter->ItemCategoryFilter)); J.WriteValue("ItemAttributeFilter", UEnum::GetValueOrBitfieldAsString(Data.DataFilter->ItemAttributeFilter)); TArray FilterTypes = Data.DataFilter->ExtraFilters.GetFilterTypes(); if (FilterTypes.Num() > 0) { J.WriteArrayStart("FilterTypes"); for (const UScriptStruct* Type : FilterTypes) { J.WriteValue(Type->GetPathName()); } J.WriteArrayEnd(); } J.WriteObjectEnd(); J.Close(); } FStudioTelemetry::Get().RecordEvent(TEXT("Editor.AssetView.BackendFilter"), { { TEXT("SchemaVersion"), 1 }, { TEXT("ViewCorrelationGuid"), Data.ViewCorrelationGuid }, { TEXT("FilterSessionCorrelationGuid"), Data.FilterSessionCorrelationGuid }, { TEXT("HasCustomItemSources"), Data.bHasCustomItemSources }, { TEXT("RefreshSourceItemsDurationSeconds"), Data.RefreshSourceItemsDurationSeconds }, { TEXT("NumBackendItems"), Data.NumBackendItems }, { TEXT("DataFilter"), FJsonFragment(MoveTemp(DataFilterText)) }, }); }); Router.OnTelemetry([this](const FFrontendFilterTelemetry& Data) { FString FilterText = LexToString(FJsonNull{}); if (Data.FrontendFilters.IsValid() && Data.FrontendFilters->Num()) { Private::FAnalyticsJsonWriter J(&FilterText); J.WriteArrayStart(); for (int32 i=0; i < Data.FrontendFilters->Num(); ++i) { TSharedPtr> Filter = Data.FrontendFilters->GetFilterAtIndex(i); J.WriteValue(Filter->GetName()); } J.WriteArrayEnd(); J.Close(); } FStudioTelemetry::Get().RecordEvent(TEXT("Editor.AssetView.FrontendFilter"), { { TEXT("SchemaVersion"), 1 }, { TEXT("ViewCorrelationGuid"), Data.ViewCorrelationGuid }, { TEXT("FilterSessionCorrelationGuid"), Data.FilterSessionCorrelationGuid }, { TEXT("TotalItemsToFilter"), Data.TotalItemsToFilter }, { TEXT("PriorityItemsToFilter"), Data.PriorityItemsToFilter }, { TEXT("TotalResults"), Data.TotalResults }, { TEXT("AmortizeDurationSeconds"), Data.AmortizeDuration }, { TEXT("WorkDurationSeconds"), Data.WorkDuration }, { TEXT("ResultLatency"), AnalyticsOptionalToStringOrNull(Data.ResultLatency) }, { TEXT("TimeUntilInteractionSeconds"), AnalyticsOptionalToStringOrNull(Data.TimeUntilInteraction) }, { TEXT("Completed"), Data.bCompleted }, { TEXT("FrontendFilters"), FJsonFragment(MoveTemp(FilterText)) }, }); }); RegisterCollectionWorkflowDelegates(Router); } { using namespace UE::Telemetry::AssetRegistry; Router.OnTelemetry([this](const FStartupTelemetry& Data){ FStudioTelemetry::Get().RecordEvent(TEXT("Editor.AssetRegistry.Startup"), { { TEXT("SchemaVersion"), 1 }, { TEXT("Duration"), Data.StartupDuration }, { TEXT("StartedAsyncGather"), Data.bStartedAsyncGather }, }); }); Router.OnTelemetry([this](const FSynchronousScanTelemetry& Data){ if (Data.Duration < 0.5) { return; } FString DirectoriesText; { Private::FAnalyticsJsonWriter J(&DirectoriesText); J.WriteArrayStart(); for (const FString& Directory : MakeArrayView(Data.Directories).Left(100)) { J.WriteValue(Directory); } J.WriteArrayEnd(); J.Close(); } FString FilesText; { Private::FAnalyticsJsonWriter J(&FilesText); J.WriteArrayStart(); for (const FString& File : MakeArrayView(Data.Files).Left(100)) { J.WriteValue(File); } J.WriteArrayEnd(); J.Close(); } FStudioTelemetry::Get().RecordEvent(TEXT("Editor.AssetRegistry.SynchronousScan"), { { TEXT("SchemaVersion"), 1 }, { TEXT("Directories"), FJsonFragment(MoveTemp(DirectoriesText)) }, { TEXT("Files"), FJsonFragment(MoveTemp(FilesText)) }, { TEXT("Flags"), LexToString(Data.Flags) }, { TEXT("NumFoundAssets"), Data.NumFoundAssets }, { TEXT("DurationSeconds"), Data.Duration }, { TEXT("InitialSearchStarted"), Data.bInitialSearchStarted }, { TEXT("InitialSearchCompleted"), Data.bInitialSearchCompleted }, { TEXT("AdditionalMountSearchInProgress"), Data.bAdditionalMountSearchInProgress }, }); }); Router.OnTelemetry([this](const FGatherTelemetry& Data){ FStudioTelemetry::Get().RecordEvent(TEXT("Editor.AssetRegistry.InitialScan"), { { TEXT("SchemaVersion"), 1 }, { TEXT("TotalDurationSeconds"), Data.TotalSearchDurationSeconds }, { TEXT("TotalWorkSeconds"), Data.TotalWorkTimeSeconds }, { TEXT("DiscoverySeconds"), Data.DiscoveryTimeSeconds }, { TEXT("GatherSeconds"), Data.GatherTimeSeconds }, { TEXT("StoreSeconds"), Data.StoreTimeSeconds }, { TEXT("NumCachedDirectories"), Data.NumCachedDirectories }, { TEXT("NumUncachedDirectories"), Data.NumUncachedDirectories }, { TEXT("NumCachedAssetFiles"), Data.NumCachedAssetFiles }, { TEXT("NumUncachedAssetFiles"), Data.NumUncachedAssetFiles }, }); }); Router.OnTelemetry([this](const FDirectoryWatcherUpdateTelemetry& Data){ if (Data.DurationSeconds < 0.5) { return; } FStudioTelemetry::Get().RecordEvent(TEXT("Editor.AssetRegistry.DirectoryWatcherUpdate"), { { TEXT("SchemaVersion"), 1 }, { TEXT("NumChanges"), Data.Changes.Num() }, { TEXT("DurationSeconds"), Data.DurationSeconds }, { TEXT("InitialSearchStarted"), Data.bInitialSearchStarted }, { TEXT("InitialSearchCompleted"), Data.bInitialSearchCompleted }, }); }); Router.OnTelemetry([this](const FFileJournalErrorTelemetry& Data){ FStudioTelemetry::Get().RecordEvent(TEXT("Editor.AssetRegistry.FileJournalError"), { { TEXT("SchemaVersion"), 1 }, { TEXT("Directory"), Data.Directory }, { TEXT("ErrorString"), Data.ErrorString }, }); }); Router.OnTelemetry([this](const FFileJournalWrappedTelemetry& Data) { FStudioTelemetry::Get().RecordEvent(TEXT("Editor.AssetRegistry.FileJournalWrapped"), { { TEXT("SchemaVersion"), 1 }, { TEXT("VolumeName"), Data.VolumeName}, { TEXT("JournalMaximumSize"), Data.JournalMaximumSize}, }); }); } { UE::Virtualization::GetAnalyticsRecordEvent().AddLambda([](const FString& EventName, const TArray& Attributes, UE::Virtualization::EAnalyticsFlags Flags) { FStudioTelemetry::Get().RecordEvent(EventName, Attributes); if (EnumHasAllFlags(Flags, UE::Virtualization::EAnalyticsFlags::Flush)) { FStudioTelemetry::Get().FlushEvents(); } }); } } void FEditorTelemetry::EndSession() { if (EditorSpan.IsValid()) { FStudioTelemetry::Get().EndSpan(EditorSpan); EditorSpan.Reset(); } }