Skip to content

fix: pause app hang detection when going on the background - #1506

Closed
bitsandfoxes wants to merge 5 commits into
mainfrom
fix/pause-app-hang
Closed

fix: pause app hang detection when going on the background#1506
bitsandfoxes wants to merge 5 commits into
mainfrom
fix/pause-app-hang

Conversation

@bitsandfoxes

Copy link
Copy Markdown
Contributor

Relies on getsentry/sentry-native#1928

To prevent false positives like reporting an app hang event because the game went on the background.

@bitsandfoxes
bitsandfoxes marked this pull request as draft July 30, 2026 09:16
@bitsandfoxes
bitsandfoxes marked this pull request as ready for review July 31, 2026 09:15

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1e1ac54. Configure here.

Comment thread plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp
Comment thread plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp
Comment thread CHANGELOG.md Outdated
Comment on lines +646 to 656
{
AppHangPauseFunc = reinterpret_cast<void (*)()>(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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: A data race exists on AppHangHeartbeatFunc and AppHangPauseFunc as they are accessed from multiple threads without atomic or mutex protection, leading to undefined behavior.
Severity: MEDIUM

Suggested Fix

Protect the reads and writes to AppHangHeartbeatFunc and AppHangPauseFunc within ResolveAppHangFunctions(). This can be achieved by using a FCriticalSection to lock around the check and subsequent dlsym calls. Alternatively, convert the function pointers to TAtomic<TFunction<...>> or a similar atomic type.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp#L624-L656

Potential issue: A data race exists on the non-atomic function pointers
`AppHangHeartbeatFunc` and `AppHangPauseFunc`. The function `ResolveAppHangFunctions()`,
which reads and writes these pointers, is called from the game thread via
`PumpAppHangHeartbeat()` and potentially from a different thread via
`PauseAppHangTracking()`. This concurrent access without a mutex or atomic operations is
undefined behavior. The initial check `if (AppHangHeartbeatFunc && AppHangPauseFunc)` is
a racy read. While the practical impact might be limited to redundant `dlsym` calls on
some hardware, a torn pointer read could theoretically lead to a crash when the garbage
pointer is called.

@tustanivsky

Copy link
Copy Markdown
Collaborator

Here are several findings about Unreal's app‑lifecycle delegates that are worth considering here:

  1. On Android, the ApplicationWillDeactivateDelegate/ApplicationHasReactivatedDelegate and ApplicationWillEnterBackgroundDelegate/ApplicationHasEnteredForegroundDelegate delegate pairs are always broadcast together back‑to‑back in the same suspend/resume callback so subscribing to both isn't necessary.

  2. On desktop (Win/Linux/Mac), none of these delegates are broadcast by the engine so the subscriptions are effectively no‑ops there.

  3. On consoles, behavior may vary per platform:

  • PS5 - only ApplicationWillDeactivateDelegate/ApplicationHasReactivatedDelegate are used for app‑state changes (WillEnterBackground is never broadcast).
  • Xbox - both pairs fire, but for different scenarios: deactivate/reactivate maps to constrained (lost focus but still ticking, just throttled, the main false‑positive case), while background/foreground maps to suspend/resume. Subscribing to both makes sense here.
  • Switch - both pairs are used, but the engine restores state based only on the immediately previous focus state, so on resume it fires only the single matching counterpart which means some sequences can leave one delegate unpaired. For example,
    overlay appears → app deactivated (WillDeactivate) → user presses Home → app backgrounded (WillEnterBackground) → user reopens → Unreal sees previous state = background and fires only ApplicationHasEnteredForegroundDelegate.
    The earlier deactivation is never matched by HasReactivated, so the corresponding flag stays "paused" and hang tracking may not resume for the rest of the session.

Given these differences, I'd suggest reshaping the generic‑platform implementation so that downstream plugin extensions can control which delegates they subscribe to and how they map to pause/resume, e.g. a virtual ConfigureAppHangTracking() where console extensions override it as needed (PS5 → single pair; Xbox → both pairs, Switch → custom handling that avoids the stuck‑flag case).

Also, for Xbox and Switch we should probably validate this behavior on actual devkits and see whether hiding/resuming the app actually produces false‑positive hangs since it's something hard to confirm only by looking into engine sources.

# Conflicts:
#	CHANGELOG.md
@tustanivsky

Copy link
Copy Markdown
Collaborator

Superseded by #1523

@tustanivsky tustanivsky closed this Aug 7, 2026
@tustanivsky
tustanivsky deleted the fix/pause-app-hang branch August 10, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants