diff --git a/CHANGELOG.md b/CHANGELOG.md index bdf325064..af71241e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- The app hang detection now correctly pauses when the game switched from foreground to background. This prevents false positive app hang events ([#1506](https://github.com/getsentry/sentry-unreal/pull/1506)) - Fix Android `minifyRelease` R8 build failure caused by missing `android.os.ProfilingManager`/`ProfilingResult` class references in the Java SDK ([#1505](https://github.com/getsentry/sentry-unreal/pull/1505)) ### Dependencies diff --git a/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp index e24b3f556..3a3df953f 100644 --- a/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp @@ -132,10 +132,38 @@ void FAndroidSentrySubsystem::InitWithSettings(const USentrySettings* settings, if (IsEnabled() && bNdkAppHangTracking) { + bAppIsActive.AtomicSet(true); + bAppIsForeground.AtomicSet(true); + // OnEndFrame is broadcast on the game thread, so the first heartbeat latches it as the // monitored thread and every subsequent frame refreshes the heartbeat sentry-native watches // for staleness before capturing an app-hang event. - OnEndFrameDelegateHandle = FCoreDelegates::OnEndFrame.AddRaw(this, &FAndroidSentrySubsystem::PumpAppHangHeartbeat); + OnEndFrameDelegateHandle = FCoreDelegates::OnEndFrame.AddLambda([this]() + { + // Prevent a late frame callback from rearming the watchdog after the app becomes inactive. + if (bAppIsActive && bAppIsForeground) + { + PumpAppHangHeartbeat(); + } + }); + OnWillDeactivateDelegateHandle = FCoreDelegates::ApplicationWillDeactivateDelegate.AddLambda([this]() + { + bAppIsActive.AtomicSet(false); + PauseAppHangTracking(); + }); + OnWillEnterBackgroundDelegateHandle = FCoreDelegates::ApplicationWillEnterBackgroundDelegate.AddLambda([this]() + { + bAppIsForeground.AtomicSet(false); + PauseAppHangTracking(); + }); + OnHasEnteredForegroundDelegateHandle = FCoreDelegates::ApplicationHasEnteredForegroundDelegate.AddLambda([this]() + { + bAppIsForeground.AtomicSet(true); + }); + OnHasReactivatedDelegateHandle = FCoreDelegates::ApplicationHasReactivatedDelegate.AddLambda([this]() + { + bAppIsActive.AtomicSet(true); + }); } } @@ -153,6 +181,30 @@ void FAndroidSentrySubsystem::Close() OnEndFrameDelegateHandle.Reset(); } + if (OnWillDeactivateDelegateHandle.IsValid()) + { + FCoreDelegates::ApplicationWillDeactivateDelegate.Remove(OnWillDeactivateDelegateHandle); + OnWillDeactivateDelegateHandle.Reset(); + } + + if (OnWillEnterBackgroundDelegateHandle.IsValid()) + { + FCoreDelegates::ApplicationWillEnterBackgroundDelegate.Remove(OnWillEnterBackgroundDelegateHandle); + OnWillEnterBackgroundDelegateHandle.Reset(); + } + + if (OnHasEnteredForegroundDelegateHandle.IsValid()) + { + FCoreDelegates::ApplicationHasEnteredForegroundDelegate.Remove(OnHasEnteredForegroundDelegateHandle); + OnHasEnteredForegroundDelegateHandle.Reset(); + } + + if (OnHasReactivatedDelegateHandle.IsValid()) + { + FCoreDelegates::ApplicationHasReactivatedDelegate.Remove(OnHasReactivatedDelegateHandle); + OnHasReactivatedDelegateHandle.Reset(); + } + FSentryJavaObjectWrapper::CallStaticMethod(SentryJavaClasses::Sentry, "flush", "(J)V", (jlong)3000); FSentryJavaObjectWrapper::CallStaticMethod(SentryJavaClasses::Sentry, "close", "()V"); } @@ -551,24 +603,55 @@ FString FAndroidSentrySubsystem::TryCaptureScreenshot() const void FAndroidSentrySubsystem::PumpAppHangHeartbeat() { - if (!AppHangHeartbeatFunc) + ResolveAppHangFunctions(); + + if (AppHangHeartbeatFunc) + { + AppHangHeartbeatFunc(); + } +} + +void FAndroidSentrySubsystem::PauseAppHangTracking() +{ + ResolveAppHangFunctions(); + + if (AppHangPauseFunc) + { + AppHangPauseFunc(); + } +} + +void FAndroidSentrySubsystem::ResolveAppHangFunctions() +{ + if (AppHangHeartbeatFunc && AppHangPauseFunc) { - // libsentry.so is loaded asynchronously by the Java SDK (io.sentry.ndk.SentryNdk), so resolve - // the symbol lazily against the already-loaded library rather than linking it at build time. - if (void* libsentryHandle = dlopen("libsentry.so", RTLD_NOLOAD | RTLD_NOW)) + return; + } + + // libsentry.so is loaded asynchronously by the Java SDK (io.sentry.ndk.SentryNdk), so resolve + // symbols lazily against the already-loaded library rather than linking them at build time. + if (void* libsentryHandle = dlopen("libsentry.so", RTLD_NOLOAD | RTLD_NOW)) + { + if (!AppHangHeartbeatFunc) { AppHangHeartbeatFunc = reinterpret_cast(dlsym(libsentryHandle, "sentry_app_hang_heartbeat")); - dlclose(libsentryHandle); if (AppHangHeartbeatFunc) { UE_LOG(LogSentrySdk, Log, TEXT("Resolved sentry_app_hang_heartbeat for NDK app-hang tracking.")); } } - } - if (AppHangHeartbeatFunc) - { - AppHangHeartbeatFunc(); + if (!AppHangPauseFunc) + { + AppHangPauseFunc = reinterpret_cast(dlsym(libsentryHandle, "sentry_app_hang_pause")); + + if (AppHangPauseFunc) + { + UE_LOG(LogSentrySdk, Log, TEXT("Resolved sentry_app_hang_pause for NDK app-hang tracking.")); + } + } + + dlclose(libsentryHandle); } } diff --git a/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.h index e72f76cca..6a8337e1b 100644 --- a/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.h @@ -2,6 +2,7 @@ #pragma once +#include "HAL/ThreadSafeBool.h" #include "Interface/SentrySubsystemInterface.h" class FAndroidSentrySubsystem : public ISentrySubsystem @@ -63,15 +64,24 @@ class FAndroidSentrySubsystem : public ISentrySubsystem private: void PumpAppHangHeartbeat(); + void PauseAppHangTracking(); + void ResolveAppHangFunctions(); void (*AppHangHeartbeatFunc)() = nullptr; + void (*AppHangPauseFunc)() = nullptr; bool bNdkAppHangTracking = false; + FThreadSafeBool bAppIsActive; + FThreadSafeBool bAppIsForeground; bool isScreenshotAttachmentEnabled = false; FDelegateHandle OnHandleSystemErrorDelegateHandle; FDelegateHandle OnEndFrameDelegateHandle; + FDelegateHandle OnWillDeactivateDelegateHandle; + FDelegateHandle OnWillEnterBackgroundDelegateHandle; + FDelegateHandle OnHasEnteredForegroundDelegateHandle; + FDelegateHandle OnHasReactivatedDelegateHandle; }; typedef FAndroidSentrySubsystem FPlatformSentrySubsystem; diff --git a/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.cpp b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.cpp index 1b3832d30..778ad9418 100644 --- a/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.cpp +++ b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.cpp @@ -628,11 +628,36 @@ void FGenericPlatformSentrySubsystem::InitWithSettings(const USentrySettings* se if (bNativeHangTracking && isEnabled) { + bAppIsActive.AtomicSet(true); + bAppIsForeground.AtomicSet(true); + // OnEndFrame is broadcast on the game thread, so the first invocation latches it as the monitored // thread and every subsequent frame refreshes the heartbeat the daemon watches for staleness - AppHangHeartbeatHandle = FCoreDelegates::OnEndFrame.AddLambda([]() + AppHangHeartbeatHandle = FCoreDelegates::OnEndFrame.AddLambda([this]() + { + // Prevent a late frame callback from rearming the watchdog after the app becomes inactive. + if (bAppIsActive && bAppIsForeground) + { + sentry_app_hang_heartbeat(); + } + }); + AppHangWillDeactivateHandle = FCoreDelegates::ApplicationWillDeactivateDelegate.AddLambda([this]() { - sentry_app_hang_heartbeat(); + bAppIsActive.AtomicSet(false); + sentry_app_hang_pause(); + }); + AppHangWillEnterBackgroundHandle = FCoreDelegates::ApplicationWillEnterBackgroundDelegate.AddLambda([this]() + { + bAppIsForeground.AtomicSet(false); + sentry_app_hang_pause(); + }); + AppHangHasEnteredForegroundHandle = FCoreDelegates::ApplicationHasEnteredForegroundDelegate.AddLambda([this]() + { + bAppIsForeground.AtomicSet(true); + }); + AppHangHasReactivatedHandle = FCoreDelegates::ApplicationHasReactivatedDelegate.AddLambda([this]() + { + bAppIsActive.AtomicSet(true); }); } @@ -686,6 +711,26 @@ void FGenericPlatformSentrySubsystem::Close() FCoreDelegates::OnEndFrame.Remove(AppHangHeartbeatHandle); AppHangHeartbeatHandle.Reset(); } + if (AppHangWillDeactivateHandle.IsValid()) + { + FCoreDelegates::ApplicationWillDeactivateDelegate.Remove(AppHangWillDeactivateHandle); + AppHangWillDeactivateHandle.Reset(); + } + if (AppHangWillEnterBackgroundHandle.IsValid()) + { + FCoreDelegates::ApplicationWillEnterBackgroundDelegate.Remove(AppHangWillEnterBackgroundHandle); + AppHangWillEnterBackgroundHandle.Reset(); + } + if (AppHangHasEnteredForegroundHandle.IsValid()) + { + FCoreDelegates::ApplicationHasEnteredForegroundDelegate.Remove(AppHangHasEnteredForegroundHandle); + AppHangHasEnteredForegroundHandle.Reset(); + } + if (AppHangHasReactivatedHandle.IsValid()) + { + FCoreDelegates::ApplicationHasReactivatedDelegate.Remove(AppHangHasReactivatedHandle); + AppHangHasReactivatedHandle.Reset(); + } isEnabled = false; diff --git a/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.h b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.h index f2c6fa571..72652bd21 100644 --- a/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.h +++ b/plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.h @@ -159,7 +159,14 @@ class FGenericPlatformSentrySubsystem : public ISentrySubsystem bool bNativeHangTracking; + FThreadSafeBool bAppIsActive; + FThreadSafeBool bAppIsForeground; + FDelegateHandle AppHangHeartbeatHandle; + FDelegateHandle AppHangWillDeactivateHandle; + FDelegateHandle AppHangWillEnterBackgroundHandle; + FDelegateHandle AppHangHasEnteredForegroundHandle; + FDelegateHandle AppHangHasReactivatedHandle; FDateTime initTimestamp;