Files
UnrealEngineUWP/Engine/Source/Programs/CrashReportClient/Private/CrashReportAnalyticsSessionSummary.cpp
Devin Doucette 3045e3c75f Logging: Changed CanBeUsedOnPanicThread() to return false by default
The previous default of CanBeUsedOnAnyThread() proved unsafe since some output devices can safely be used on any thread but cannot safely be used during a crash. CanBeUsedOnAnyThread() was used pre-5.1 to control serialization to an output device during a crash, but optimizations in 5.1 have uncovered that some of these were never safe.

#preflight 62856506614041edb7a6de4b
#rb Zousar.Shaker
#rnx

[CL 20280354 by Devin Doucette in ue5-main branch]
2022-05-19 10:39:18 -04:00

743 lines
29 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "CrashReportAnalyticsSessionSummary.h"
#include "AnalyticsPropertyStore.h"
#include "AnalyticsSessionSummaryManager.h"
#include "AnalyticsSessionSummarySender.h"
#include "IAnalyticsProviderET.h"
#include "CrashReportAnalytics.h"
#include "Containers/Map.h"
#include "GenericPlatform/GenericPlatformMath.h"
#include "GenericPlatform/GenericPlatformCrashContext.h"
#include "HAL/PlatformTime.h"
#include "HAL/PlatformProcess.h"
#include "HAL/Thread.h"
#include "Internationalization/Regex.h"
#include "Logging/LogMacros.h"
#include "Misc/EngineVersion.h"
#include "Misc/Paths.h"
#include "Misc/ScopeLock.h"
#include "Misc/CoreDelegates.h"
#include "Misc/OutputDeviceRedirector.h"
#include "Templates/UnrealTemplate.h"
DEFINE_LOG_CATEGORY_STATIC(LogCrashReportClientDiagnostics, Log, All)
#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
#include "Windows.h"
/** Handle windows messages. */
LRESULT CALLBACK CrashReportAnalyticsSessionSummaryWindowProc(HWND Hwnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
// wParam is true if the user session is going away (and CRC is going to die)
if (Msg == WM_ENDSESSION && wParam == TRUE)
{
FCrashReportAnalyticsSessionSummary::Get().OnUserLoggingOut();
}
else if (Msg == WM_CLOSE)
{
FCrashReportAnalyticsSessionSummary::Get().OnQuitSignal();
}
return DefWindowProc(Hwnd, Msg, wParam, lParam);
}
FProcHandle OpenProcessForMonitoring(uint32 pid)
{
// Until a crash occurs, for security reasons, restrict CRC accesses on the remote process.
return FProcHandle(::OpenProcess(PROCESS_DUP_HANDLE | PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE | SYNCHRONIZE, 0, pid));
}
#include "Windows/HideWindowsPlatformTypes.h"
#else // PLATFORM_WINDOWS
FProcHandle OpenProcessForMonitoring(uint32 pid)
{
return FPlatformProcess::OpenProcess(pid);
}
#endif // PLATFORM_WINDOWS
namespace CrcAnalyticsProperties
{
// NOTE: Update this when you add/remove/change key behavior. That's useful to track how one changes affects metrics in-dev where users don't always have an engine versions.
// - V3 -> Windows optimization for stall/ensure -> The engine only captures the responsible thread so CRC walks 1 thread rather than all threads.
// - V4 -> Stripped ensure callstack from the ensure error message to remove noise in the diagnostic log.
// - V5 -> Measured time to stack-walk, gather files and addded stall count.
// - V6 -> Added MonitorTickCount, MonitorQueryingPipe and App/Death log.
// - V7 -> Removed MonitorQueryingPipe. It was added to detect if CRC crashed while reading the pipe. Data showed that wasn't the case.
constexpr uint32 CrcAnalyticsSummaryVersion = 7;
/** The exit code of the monitored application. */
static const TAnalyticsProperty<int32> MonitoredAppExitCode(TEXT("ExitCode"));
/** Track when CRC detected the death of the monitored app. */
static const TAnalyticsProperty<FDateTime> MonitoredAppDeathTimestamp(TEXT("DeathTimestamp"));
/** CRC engine version. In-dev, people don't always recompile CRC and we get disparity between the monitored app and CRC version. */
static const TAnalyticsProperty<FString> EngineVersion(TEXT("MonitorEngineVersion"));
/** The version number of the key/set used by CRC. */
static const TAnalyticsProperty<uint32> SummaryVersionNumber(TEXT("MonitorSummaryVersion"));
/** The CRC startup timestamp. */
static const TAnalyticsProperty<FDateTime> StartupTimestamp(TEXT("MonitorStartupTimestamp"));
/** The CRC timestamp. */
static const TAnalyticsProperty<FDateTime> Timestamp(TEXT("MonitorTimestamp"));
/** Number of time CRC analytic thread ticked. */
static const TAnalyticsProperty<uint32> TickCount(TEXT("MonitorTickCount"));
/** If CRC raised an exception that was captured by SEH, this is the exception code. */
static const TAnalyticsProperty<int32> ExceptCode(TEXT("MonitorExceptCode"));
/** If CRC is about to close because the system sent a quit signal. */
static const TAnalyticsProperty<bool> QuitSignalRecv(TEXT("MonitorQuitSignalRecv"));
/** The CRC diagnostic logs. */
static const TAnalyticsProperty<FString> DiagnosticLogs(TEXT("MonitorLog"));
/** The CRC session duration in seconds. */
static const TAnalyticsProperty<int32> SessionDurationSecs(TEXT("MonitorSessionDuration"));
/** The battery level, if known. */
static const TAnalyticsProperty<uint32> BatteryLevel(TEXT("MonitorBatteryLevel"));
/** If the system is connected to AC, if know */
static const TAnalyticsProperty<bool> IsOnACPower(TEXT("MonitorOnACPower"));
/** Whether CRC is reporting a crash. */
static const TAnalyticsProperty<bool> IsReportingCrash(TEXT("MonitorIsReportingCrash"));
/** Whether CRC is collecting crash artifacts. */
static const TAnalyticsProperty<bool> IsCollectingCrash(TEXT("MonitorIsCollectingCrash"));
/** Whether CRC is processing a crash. */
static const TAnalyticsProperty<bool> IsProcessingCrash(TEXT("MonitorIsProcessingCrash"));
/** If CRC is about to be killed because the user is logging out (system shutdown/reboot). */
static const TAnalyticsProperty<bool> UserIsLoggingOut(TEXT("MonitorLoggingOut"));
/** Whether CRC crashed. */
static const TAnalyticsProperty<bool> IsCrashing(TEXT("MonitorCrashed"));
/** Whether CRC was shutdown. */
static const TAnalyticsProperty<bool> WasShutdown(TEXT("MonitorWasShutdown"));
/** Number of crash event passed to CRC. (Ensure, Assert, Crash, etc). */
static const TAnalyticsProperty<uint32> ReportCount(TEXT("MonitorReportCount"));
/** Number of ensures handled by CRC. */
static const TAnalyticsProperty<uint32> EnsureCount(TEXT("MonitorEnsureCount"));
/** Number of assert handled by CRC.*/
static const TAnalyticsProperty<uint32> AssertCount(TEXT("MonitorAssertCount"));
/** Number of stalls handed by CRC. */
static const TAnalyticsProperty<uint32> StallCount(TEXT("MonitorStallCount"));
/** The worst unattended report time measured (if any). The user is not involved, so it measure how fast CRC can process a crash, especially ensures and stalls. */
static const TAnalyticsProperty<float> LonguestUnattendedReportSecs(TEXT("MonitorLongestUnattendedReportSecs"));
}
namespace CrashReportClientUtils
{
/** Flush the store peridically. */
static const FTimespan PropertyStoreFlushPeriod = FTimespan::FromSeconds(10);
/** Tick periodically to poll new information. */
static const FTimespan TickPeriod = FTimespan::FromSeconds(0.5);
/** Maximum length of the diagnostic log. */
static constexpr int32 MaxDiagnosticLogLen = 8 * 1024;
#if PLATFORM_WINDOWS && CRASH_REPORT_WITH_MTBF
HWND Hwnd = NULL;
/** Create a hidden Windows to intercept WM_ messages, especially the WM_ENDSESSION. */
void InitPlatformSpecific()
{
// Register the window class.
const wchar_t CLASS_NAME[] = L"CRC Analytics Session Window Message Interceptor";
WNDCLASS wc = { };
wc.lpfnWndProc = CrashReportAnalyticsSessionSummaryWindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc);
// Create a window to capture WM_ENDSESSION message (so that we can detect when CRC fails because the user is logging off/shutting down/restarting)
Hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"CRC Message Loop Wnd", // Window text
WS_OVERLAPPEDWINDOW, // Window style
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, // Size and position
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
NULL // Additional application data
);
}
/** Pump the message from the hidden Windows. */
void TickPlatformSpecific()
{
if (Hwnd != NULL)
{
// Pump the messages.
MSG Msg = { };
while (::PeekMessage(&Msg, NULL, 0, 0, PM_REMOVE))
{
::TranslateMessage(&Msg);
::DispatchMessage(&Msg);
}
}
}
bool GetPowerStatus(TOptional<bool>& OutACPowerConnected, TOptional<uint32>& OutBatteryPct)
{
bool bAvailable = false;
SYSTEM_POWER_STATUS Status;
if (GetSystemPowerStatus(&Status))
{
switch (Status.ACLineStatus)
{
case 0: // AC Offline
OutACPowerConnected.Emplace(false);
bAvailable = true;
break;
case 1: // AC Online
OutACPowerConnected.Emplace(true);
bAvailable = true;
break;
default: // Unknown
break;
}
if (Status.BatteryLifePercent != 255) // Unknown
{
OutBatteryPct.Emplace(Status.BatteryLifePercent);
bAvailable = true;
}
}
return bAvailable;
}
#else
void InitPlatformSpecific(){}
void TickPlatformSpecific(){}
bool GetPowerStatus(TOptional<bool>& OutACPowerConnected, TOptional<uint32>& OutBatteryPct) { return false; }
#endif
} // CrashReportClientUtils
/**
* Augments the default summary senders to perform a short analytis to detect if this is the session ended abnormally. The sender searches
* for very specific keys published by the engine. Remember that CRC merges its summary with the monitored process summary (the Editor), so
* it gets to see what was recorded (or not) by the monitored process.
*/
class FCrashReportClientAnalyticsSessionSummarySender : public FAnalyticsSessionSummarySender
{
public:
FCrashReportClientAnalyticsSessionSummarySender(IAnalyticsProviderET& Provider)
: FAnalyticsSessionSummarySender(Provider)
{
}
virtual bool SendSessionSummary(const FString& UserId, const FString& AppId, const FString& AppVersion, const FString& SessionId, const TArray<FAnalyticsEventAttribute>& Properties) override
{
// CRC should only send one session (its own), but do a reset in case this convention changes.
bAbnormalShutdown = false;
bUserLoggingOut = false;
// Analyze the report to be sent and try to figure out if this is an abnormal shutdown. They keys are taken from the engine analytics session summary. To prevent dependencies
// between CRC and Engine analytics, we duplicate the keys here.
if (const FAnalyticsEventAttribute* ShutdownTypeCode = Properties.FindByPredicate([](const FAnalyticsEventAttribute& Candidate) { return Candidate.GetName() == FAnalyticsSessionSummaryManager::ShutdownTypeCodeProperty.Key; }))
{
bAbnormalShutdown = (ShutdownTypeCode->GetValue() == LexToString((int32)EAnalyticsSessionShutdownType::Abnormal));
if (bAbnormalShutdown)
{
if (const FAnalyticsEventAttribute* UserLoggingOut = Properties.FindByPredicate([](const FAnalyticsEventAttribute& Candidate) { return Candidate.GetName() == FAnalyticsSessionSummaryManager::IsUserLoggingOutProperty.Key; }))
{
bUserLoggingOut = (UserLoggingOut->GetValue() == LexToString(true));
}
}
}
// Send the report unmodified.
return FAnalyticsSessionSummarySender::SendSessionSummary(UserId, AppId, AppVersion, SessionId, Properties);
}
/** Returns whether the last session sent was abnormally terminated. */
bool IsAbnormalShutdown() const
{
return bAbnormalShutdown && !bUserLoggingOut;
}
private:
bool bAbnormalShutdown = false;
bool bUserLoggingOut = false;
};
FCrashReportAnalyticsSessionSummary::FCrashReportAnalyticsSessionSummary()
: SessionStartTimeSecs(FPlatformTime::Seconds())
, bMonitoredAppDeathRecorded(false)
, bShutdown(false)
{
if (IsEnabled())
{
CrashReportClientUtils::InitPlatformSpecific();
// Reserve the memory for the log string.
DiagnosticLog.Reset(CrashReportClientUtils::MaxDiagnosticLogLen);
DiagnosticLog.Append(FString::Printf(TEXT("CRC/Init:%s"), *FDateTime::UtcNow().ToString()));
}
}
FCrashReportAnalyticsSessionSummary& FCrashReportAnalyticsSessionSummary::FCrashReportAnalyticsSessionSummary::Get()
{
static FCrashReportAnalyticsSessionSummary Instance;
return Instance;
}
void FCrashReportAnalyticsSessionSummary::Initialize(const FString& ProcessGroupId, uint32 ForProcessId)
{
if (IsEnabled() && !SessionSummaryManager && !ProcessGroupId.IsEmpty())
{
SessionSummaryManager = MakeUnique<FAnalyticsSessionSummaryManager>(TEXT("CrashReportClient"), ProcessGroupId, ForProcessId);
if (SessionSummaryManager)
{
constexpr uint32 ReservedFileCapacity = CrashReportClientUtils::MaxDiagnosticLogLen + (4 * 1024);
PropertyStore = SessionSummaryManager->MakeStore(ReservedFileCapacity);
if (PropertyStore)
{
FCoreDelegates::ApplicationWillTerminateDelegate.AddRaw(this, &FCrashReportAnalyticsSessionSummary::OnApplicationWillTerminate);
FCoreDelegates::OnHandleSystemError.AddRaw(this, &FCrashReportAnalyticsSessionSummary::OnHandleSystemError);
CrcAnalyticsProperties::EngineVersion.Set(PropertyStore.Get(), FEngineVersion::Current().ToString(EVersionComponent::Changelist));
CrcAnalyticsProperties::SummaryVersionNumber.Set(PropertyStore.Get(), CrcAnalyticsProperties::CrcAnalyticsSummaryVersion);
CrcAnalyticsProperties::StartupTimestamp.Set(PropertyStore.Get(), FDateTime::UtcNow());
CrcAnalyticsProperties::Timestamp.Set(PropertyStore.Get(), FDateTime::UtcNow());
CrcAnalyticsProperties::TickCount.Set(PropertyStore.Get(), 0);
CrcAnalyticsProperties::SessionDurationSecs.Set(PropertyStore.Get(), FMath::FloorToInt(static_cast<float>(FPlatformTime::Seconds() - SessionStartTimeSecs)));
CrcAnalyticsProperties::DiagnosticLogs.Set(PropertyStore.Get(), DiagnosticLog, CrashReportClientUtils::MaxDiagnosticLogLen);
CrcAnalyticsProperties::IsReportingCrash.Set(PropertyStore.Get(), false);
CrcAnalyticsProperties::IsCollectingCrash.Set(PropertyStore.Get(), false);
CrcAnalyticsProperties::IsProcessingCrash.Set(PropertyStore.Get(), false);
CrcAnalyticsProperties::UserIsLoggingOut.Set(PropertyStore.Get(), false);
CrcAnalyticsProperties::IsCrashing.Set(PropertyStore.Get(), false);
CrcAnalyticsProperties::WasShutdown.Set(PropertyStore.Get(), false);
CrcAnalyticsProperties::QuitSignalRecv.Set(PropertyStore.Get(), false);
CrcAnalyticsProperties::ReportCount.Set(PropertyStore.Get(), 0);
CrcAnalyticsProperties::EnsureCount.Set(PropertyStore.Get(), 0);
CrcAnalyticsProperties::AssertCount.Set(PropertyStore.Get(), 0);
CrcAnalyticsProperties::StallCount.Set(PropertyStore.Get(), 0);
UpdatePowerStatus();
Flush();
GLog->AddOutputDevice(this);
// CRC main thread might be busy processing a crash, so use a background thread to record important events that could otherwise be missed.
AnalyticsThread = MakeUnique<FThread>(TEXT("AnalyticsMonitorThread"), [this, ForProcessId]()
{
// Try to open the process.
FProcHandle MonitoredProcessHandle = OpenProcessForMonitoring(ForProcessId);
if (!MonitoredProcessHandle.IsValid())
{
LogEvent(TEXT("CRC/OpenProcessFailed"));
return;
}
CrashReportClientUtils::InitPlatformSpecific();
double NextFlushTimeSecs = FPlatformTime::Seconds();
bool bFlushedLowBattery = false;
LogEvent(TEXT("CRC/Monitoring")); // About to enter the loop.
while (!bShutdown)
{
CrcAnalyticsProperties::TickCount.Update(PropertyStore.Get(), [](uint32& Actual) { ++Actual; return true; });
CrashReportClientUtils::TickPlatformSpecific();
// Monitor the power level.
bool bShouldFlush = UpdatePowerStatus();
if (!FPlatformProcess::IsProcRunning(MonitoredProcessHandle))
{
OnMonitoredAppDeath(MonitoredProcessHandle);
break;
}
else if (FPlatformTime::Seconds() > NextFlushTimeSecs || bShouldFlush)
{
// Flush to timestamp the session.
Flush();
NextFlushTimeSecs += CrashReportClientUtils::PropertyStoreFlushPeriod.GetTotalSeconds();
}
// Throttle the thread.
FPlatformProcess::Sleep(CrashReportClientUtils::TickPeriod.GetTotalSeconds());
}
FPlatformProcess::CloseProc(MonitoredProcessHandle);
});
}
}
}
}
bool FCrashReportAnalyticsSessionSummary::IsValid() const
{
return IsEnabled() && PropertyStore.IsValid();
}
void FCrashReportAnalyticsSessionSummary::Shutdown(IAnalyticsProviderET* AnalyticsProvider, TFunction<void()> HandleAbnormalShutdownFn)
{
if (!IsValid())
{
return;
}
if (AnalyticsThread)
{
bShutdown = true;
AnalyticsThread->Join();
AnalyticsThread.Reset();
}
CrcAnalyticsProperties::WasShutdown.Set(PropertyStore.Get(), true);
LogEvent(FString::Printf(TEXT("CRC/Shutdown:%s:%.1fs"), *FDateTime::UtcNow().ToString(), FPlatformTime::Seconds() - SessionStartTimeSecs));
// Unregister from the core.
FCoreDelegates::ApplicationWillTerminateDelegate.RemoveAll(this);
FCoreDelegates::OnHandleSystemError.RemoveAll(this);
GLog->RemoveOutputDevice(this);
// Flush and close CRC analytics summary.
Flush();
PropertyStore.Reset();
// If CRC is allowed to send summary report on behalf of the monitored application.
if (AnalyticsProvider)
{
// Set a summary sender that will intercept the set of properties and perform an analysis to detect an abnormal shutdown.
TSharedPtr<FCrashReportClientAnalyticsSessionSummarySender> SummarySender = MakeShared<FCrashReportClientAnalyticsSessionSummarySender>(*AnalyticsProvider);
SessionSummaryManager->SetSender(SummarySender);
// Merge the principal process summary with CRC process summary and sends the session summary if CRC is the last process to exit.
SessionSummaryManager->Shutdown();
// If the summary sender detected an abnormal shutdown by checking the summary properties, report it.
if (SummarySender->IsAbnormalShutdown() && HandleAbnormalShutdownFn)
{
HandleAbnormalShutdownFn();
}
}
else // Discard CRC summary.
{
SessionSummaryManager->Shutdown(/*bDiscard*/true);
}
SessionSummaryManager.Reset();
}
void FCrashReportAnalyticsSessionSummary::Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category, const double Time)
{
Serialize(V, Verbosity, Category);
}
void FCrashReportAnalyticsSessionSummary::Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category)
{
// Log the errors, especially the failed 'check()' with the callstack/message.
if (Verbosity == ELogVerbosity::Error || Verbosity == ELogVerbosity::Fatal)
{
// Log but don't forward to UE logging system. The log is already originate from the logging system.
LogEvent(TEXT("CRC/Error"), /*bForwardToUELog*/false);
LogEvent(V, /*bForwardToUELog*/false);
}
}
bool FCrashReportAnalyticsSessionSummary::CanBeUsedOnAnyThread() const
{
return true;
}
bool FCrashReportAnalyticsSessionSummary::CanBeUsedOnMultipleThreads() const
{
return true;
}
bool FCrashReportAnalyticsSessionSummary::CanBeUsedOnPanicThread() const
{
return true;
}
void FCrashReportAnalyticsSessionSummary::LogEvent(const FString& Event)
{
LogEvent(*Event);
}
void FCrashReportAnalyticsSessionSummary::LogEvent(const TCHAR* Event, bool bForwardToUELog)
{
if (IsValid())
{
FScopeLock ScopedLock(&LoggerLock);
TGuardValue<bool> ReentrantGuard(bLoggerReentrantGuard, true);
if (*ReentrantGuard) // Read the old value.
{
return; // Prevent renentrant logging.
}
AppendLog(Event);
}
// Prevent error logs coming from the logging system to be duplicated.
if (bForwardToUELog)
{
UE_LOG(LogCrashReportClientDiagnostics, Log, TEXT("%s"), Event);
}
}
void FCrashReportAnalyticsSessionSummary::AppendLog(const TCHAR* Event)
{
// Add the separator if some text is already logged.
if (DiagnosticLog.Len())
{
DiagnosticLog.Append(TEXT("|"));
}
// Rotate the log if it gets too long.
int32 FreeLen = CrashReportClientUtils::MaxDiagnosticLogLen - DiagnosticLog.Len();
int32 EventLen = FCString::Strlen(Event);
if (EventLen > FreeLen)
{
if (EventLen > CrashReportClientUtils::MaxDiagnosticLogLen)
{
DiagnosticLog.Reset(CrashReportClientUtils::MaxDiagnosticLogLen);
EventLen = CrashReportClientUtils::MaxDiagnosticLogLen;
}
else
{
DiagnosticLog.RemoveAt(0, EventLen - FreeLen, /*bAllowShrinking*/false); // Free space, remove the chars from the oldest events (in front).
}
}
// Append the log entry and dump the log to the file.
DiagnosticLog.AppendChars(Event, EventLen);
// Update the diagnostic field into the session summary store.
CrcAnalyticsProperties::DiagnosticLogs.Set(PropertyStore.Get(), DiagnosticLog);
// Flush the store.
Flush();
}
void FCrashReportAnalyticsSessionSummary::OnMonitoredAppDeath(FProcHandle& Handle)
{
// The first thread to exchange successfull is allowed to update. No need to update twice for the same monitored process.
bool Expected = false;
if (IsValid() && bMonitoredAppDeathRecorded.compare_exchange_strong(Expected, true))
{
CrcAnalyticsProperties::MonitoredAppDeathTimestamp.Set(PropertyStore.Get(), FDateTime::UtcNow());
LogEvent(TEXT("App/Death"));
int32 ExitCode;
if (Handle.IsValid() && FPlatformProcess::GetProcReturnCode(Handle, &ExitCode))
{
CrcAnalyticsProperties::MonitoredAppExitCode.Set(PropertyStore.Get(), ExitCode);
LogEvent(FString::Printf(TEXT("App/ExitCode:%d"), ExitCode));
}
else
{
CrcAnalyticsProperties::MonitoredAppExitCode.Set(PropertyStore.Get(), ECrashExitCodes::MonitoredApplicationExitCodeNotAvailable);
LogEvent(TEXT("App/ExitCode:N/A"));
}
Flush();
}
}
void FCrashReportAnalyticsSessionSummary::OnUserLoggingOut()
{
if (IsValid())
{
// The user is logging out and CRC is going to die.
CrcAnalyticsProperties::UserIsLoggingOut.Set(PropertyStore.Get(), true);
// Log the event (this also flush the session).
LogEvent(TEXT("CRC/EndSession"));
}
}
void FCrashReportAnalyticsSessionSummary::OnQuitSignal()
{
// The system has requested the app to close. (Like if the user gently kills the application)
CrcAnalyticsProperties::QuitSignalRecv.Set(PropertyStore.Get(), true);
// Log the event (this also flush the session).
LogEvent(TEXT("CRC/QuitSignal"));
}
void FCrashReportAnalyticsSessionSummary::OnCrcCrashing(int32 ExceptCode)
{
if (IsValid())
{
CrcAnalyticsProperties::IsCrashing.Set(PropertyStore.Get(), true);
CrcAnalyticsProperties::ExceptCode.Set(PropertyStore.Get(), ExceptCode);
TCHAR CrashEventLog[64];
FCString::Sprintf(CrashEventLog, TEXT("CRC/Crash:%d"), ExceptCode);
LogEvent(CrashEventLog); // This also flush the session.
}
}
void FCrashReportAnalyticsSessionSummary::Flush()
{
if (IsValid())
{
// Update the session progression.
CrcAnalyticsProperties::Timestamp.Set(PropertyStore.Get(), FDateTime::UtcNow(),
[](const FDateTime* Actual, const FDateTime& Proposed) { return Proposed > *Actual; });
CrcAnalyticsProperties::SessionDurationSecs.Set(PropertyStore.Get(), FMath::FloorToInt(FPlatformTime::Seconds() - SessionStartTimeSecs),
[](const int32* Actual, const int32& Proposed) { return Proposed > *Actual; });
// Flush the store to disk.
PropertyStore->Flush();
}
}
void FCrashReportAnalyticsSessionSummary::OnApplicationWillTerminate()
{
LogEvent(FString::Printf(TEXT("CRC/Terminate:%s"), *FDateTime::UtcNow().ToString()));
}
void FCrashReportAnalyticsSessionSummary::OnHandleSystemError()
{
CrcAnalyticsProperties::IsCrashing.Set(PropertyStore.Get(), true);
LogEvent(FString::Printf(TEXT("CRC/SysError:%s"), *FDateTime::UtcNow().ToString()));
}
void FCrashReportAnalyticsSessionSummary::OnCrashReportStarted(ECrashContextType CrashType, const TCHAR* ErrorMsg)
{
CrashReportStartTimeSecs = FPlatformTime::Seconds();
// Reset the timers for the current crash report (a safety measure).
CrashReportCollectingStartTimeSecs = CrashReportStartTimeSecs;
CrashReportStackWalkingStartTimeSecs = CrashReportStartTimeSecs;
CrashReportGatheringFilesStartTimeSecs = CrashReportStartTimeSecs;
CrashReportSignalingRemoteAppTimeSecs = CrashReportStartTimeSecs;
CrashReportProcessingStartTimeSecs = CrashReportStartTimeSecs;
CrcAnalyticsProperties::IsReportingCrash.Set(PropertyStore.Get(), true);
CrcAnalyticsProperties::ReportCount.Update(PropertyStore.Get(), [](uint32& Actual) { ++Actual; return true; });
LogEvent(FString::Printf(TEXT("Report/Start:%s"), *FDateTime::UtcNow().ToString()));
// Log the assert and ensure condition/file/line/message to the diagnostic log gathered by the analytics to enable searching/grouping them later on.
if (CrashType == ECrashContextType::Assert)
{
CrcAnalyticsProperties::AssertCount.Update(PropertyStore.Get(), [](uint32& Actual) { ++Actual; return true; });
LogEvent(FString::Printf(TEXT("Assert/Msg: %s"), ErrorMsg));
}
else if (CrashType == ECrashContextType::Ensure)
{
CrcAnalyticsProperties::EnsureCount.Update(PropertyStore.Get(), [](uint32& Actual) { ++Actual; return true; });
// Ensure messages include the ensure call stack. That's not useful for analytics, try keeping the essential only (ensure condition, file, line)
FRegexPattern Pattern(TEXT(R"(.*\[File:.*\]\s*\[Line:\s\d+\])")); // Need help with regex? Try https://regex101.com/
FRegexMatcher Matcher(Pattern, ErrorMsg);
if (Matcher.FindNext())
{
LogEvent(FString::Printf(TEXT("Ensure/Msg: %s"), *Matcher.GetCaptureGroup(0)));
}
else
{
LogEvent(FString::Printf(TEXT("Ensure/Msg: %s"), ErrorMsg));
}
}
else if (CrashType == ECrashContextType::Stall)
{
CrcAnalyticsProperties::StallCount.Update(PropertyStore.Get(), [](uint32& Actual) { ++Actual; return true; });
}
}
void FCrashReportAnalyticsSessionSummary::OnCrashReportCollecting()
{
CrashReportCollectingStartTimeSecs = FPlatformTime::Seconds();
CrcAnalyticsProperties::IsCollectingCrash.Set(PropertyStore.Get(), true);
FCrashReportAnalyticsSessionSummary::Get().LogEvent(TEXT("Report/Collect"));
}
void FCrashReportAnalyticsSessionSummary::OnCrashReportRemoteStackWalking()
{
CrashReportStackWalkingStartTimeSecs = FPlatformTime::Seconds();
}
void FCrashReportAnalyticsSessionSummary::OnCrashReportGatheringFiles()
{
CrashReportGatheringFilesStartTimeSecs = FPlatformTime::Seconds();
}
void FCrashReportAnalyticsSessionSummary::OnCrashReportSignalingAppToResume()
{
CrashReportSignalingRemoteAppTimeSecs = FPlatformTime::Seconds();
}
void FCrashReportAnalyticsSessionSummary::OnCrashReportProcessing(bool bUserInteractive)
{
CrcAnalyticsProperties::IsCollectingCrash.Set(PropertyStore.Get(), false);
CrcAnalyticsProperties::IsProcessingCrash.Set(PropertyStore.Get(), true);
CrashReportProcessingStartTimeSecs = FPlatformTime::Seconds();
bProcessingCrashUnattended = !bUserInteractive;
FCrashReportAnalyticsSessionSummary::Get().LogEvent(FString::Printf(TEXT("Report/Process:%s"), bUserInteractive ? TEXT("Interactive") : TEXT("Unattended")));
}
void FCrashReportAnalyticsSessionSummary::OnCrashReportCompleted(bool bSubmitted)
{
CrcAnalyticsProperties::IsCollectingCrash.Set(PropertyStore.Get(), false);
CrcAnalyticsProperties::IsProcessingCrash.Set(PropertyStore.Get(), false);
CrcAnalyticsProperties::IsReportingCrash.Set(PropertyStore.Get(), false);
double CurrTimeSecs = FPlatformTime::Seconds();
// Total time required to remote stack walk, gather files, respond to the monited app.
double CollectTimeSecs = CrashReportProcessingStartTimeSecs - CrashReportCollectingStartTimeSecs;
// Total time required to remote stack walk
double StackWalkSecs = CrashReportGatheringFilesStartTimeSecs - CrashReportStackWalkingStartTimeSecs;
// Total time required to gather files (copy log and generates minidump).
double GatherFileSecs = CrashReportSignalingRemoteAppTimeSecs - CrashReportGatheringFilesStartTimeSecs;
// Total time required by CRC to process the crash report (resolve symbols + showing the UI + user time to respond if the report is interactive).
double ProcessTimeSecs = CurrTimeSecs - CrashReportProcessingStartTimeSecs;
// Total time CRC main thread was used to process the crash.
double TotalTimeSecs = CurrTimeSecs - CrashReportStartTimeSecs;
if (bProcessingCrashUnattended) // No UI shown to the user that could amplify the time.
{
CrcAnalyticsProperties::LonguestUnattendedReportSecs.Set(PropertyStore.Get(), static_cast<float>(TotalTimeSecs), [](const float* Actual, const float& Proposed) { return Actual == nullptr || Proposed > *Actual; });
}
bProcessingCrashUnattended = false;
const TCHAR* Event = bSubmitted ? TEXT("Report/Sent") : TEXT("Report/Discarded");
LogEvent(FString::Printf(TEXT("%s:Walk=%.1fs:Gather=%.1fs:Collect=%.1fs:Process=%.1fs:Total=%.1fs"), Event, StackWalkSecs, GatherFileSecs, CollectTimeSecs, ProcessTimeSecs, TotalTimeSecs));
}
bool FCrashReportAnalyticsSessionSummary::UpdatePowerStatus()
{
bool bShouldFlush = false;
// Monitor the power level.
TOptional<bool> ConnectedToACPower;
TOptional<uint32> BatteryPercentage;
if (CrashReportClientUtils::GetPowerStatus(ConnectedToACPower, BatteryPercentage))
{
if (ConnectedToACPower)
{
CrcAnalyticsProperties::IsOnACPower.Set(PropertyStore.Get(), *ConnectedToACPower);
}
if (BatteryPercentage)
{
CrcAnalyticsProperties::BatteryLevel.Set(PropertyStore.Get(), *BatteryPercentage, [&bShouldFlush](const uint32* PreviousLevel, const uint32& NewLevel)
{
// Detects when the batter level goes from above to below or equal a threshonld.
constexpr uint32 LowBatteryPct = 2;
if (PreviousLevel && *PreviousLevel > LowBatteryPct && NewLevel <= LowBatteryPct)
{
// Last time, it was above the threshold, but now it dipped below, save the state before the battery runs out.
bShouldFlush = true;
}
return true;
});
}
}
return bShouldFlush;
}