Skip to content

Commit 5efbee8

Browse files
tustanivskygetsentry-botbitsandfoxes
authored
Add in-process hang tracking with Native backend on Windows, Linux, and Mac (#1427)
* Add out-of-process hang tracking with `Native` backend on Windows and Mac * Format code * Update changelog * Exclude Windows-specific API * bumped to 'feat/app-hang-linux' * moved to inproc implementation * bumped again * Bump submodule (test) * Fix setting name * Fix platform include * Fix option name * Rework hang tests * Fix test shutdown * Simplify hang tests * Fix event's IsAnr check for native * Clean up --------- Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io> Co-authored-by: bitsandfoxes <reg@bitfox.at>
1 parent 0296e26 commit 5efbee8

15 files changed

Lines changed: 99 additions & 14 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
- Add Unreal Engine 5.8 support ([#1440](https://github.com/getsentry/sentry-unreal/pull/1440))
8+
- Add opt-in native app-hang tracking on Windows, Mac and Linux ([#1427](https://github.com/getsentry/sentry-unreal/pull/1427))
89

910
### Dependencies
1011

integration-test/Integration.Desktop.Tests.ps1

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ BeforeDiscovery {
8080
# Memory overcommit makes OOM conditions unreliable to trigger in tests on Linux and macOS
8181
$TestCrashTypes = $TestCrashTypes | Where-Object { $_.Name -ne 'OutOfMemory' }
8282
}
83+
84+
# Define hang detection mechanisms to test
85+
$TestHangMechanisms = @(
86+
@{ Name = 'Engine'; ExceptionType = 'App Hanging'; ExceptionValuePattern = '^Application not responding$'; ExpectsCrashTypeTag = $true }
87+
@{ Name = 'Native'; ExceptionType = 'AppHang'; ExceptionValuePattern = 'App hung for at least \d+ ms'; ExpectsCrashTypeTag = $false }
88+
)
89+
90+
if ($IsMacOS) {
91+
if ($script:IsNativeBackend) {
92+
$TestHangMechanisms = @($TestHangMechanisms | Where-Object { $_.Name -eq 'Native' })
93+
}
94+
else {
95+
$TestHangMechanisms = @()
96+
}
97+
}
8398
}
8499

85100
BeforeAll {
@@ -378,13 +393,12 @@ Describe "Sentry Unreal Desktop Integration Tests (<Platform>)" -ForEach $TestTa
378393
}
379394
}
380395

381-
# Hang tracking is not supported on macOS (sentry-cocoa has its own AppHang detection)
382-
Context "Hang Tracking Tests" -Skip:$IsMacOS {
396+
Context "Hang Tracking Tests - <Name>" -ForEach $TestHangMechanisms {
383397
BeforeAll {
384398
$script:HangResult = $null
385399
$script:HangEvent = $null
386400

387-
Write-Host "Running hang tracking test..." -ForegroundColor Yellow
401+
Write-Host "Running hang tracking test ($($_.Name))..." -ForegroundColor Yellow
388402

389403
$appArgs = @(
390404
'-nullrhi', # Runs without graphics rendering (headless mode)
@@ -396,6 +410,9 @@ Describe "Sentry Unreal Desktop Integration Tests (<Platform>)" -ForEach $TestTa
396410
# Override default project settings
397411
$appArgs += "-ini:Engine:[/Script/Sentry.SentrySettings]:Dsn=$script:DSN"
398412
$appArgs += "-ini:Engine:[/Script/Sentry.SentrySettings]:EnableHangTracking=True"
413+
if ($_.Name -eq 'Native') {
414+
$appArgs += "-ini:Engine:[/Script/Sentry.SentrySettings]:UseNativeHangTracking=True"
415+
}
399416

400417
# -hang-capture triggers hang test scenario in the sample app
401418
$script:HangResult = Invoke-DeviceApp -ExecutablePath $script:AppPath -Arguments ((@('-hang-capture') + $appArgs) -join ' ')
@@ -449,12 +466,12 @@ Describe "Sentry Unreal Desktop Integration Tests (<Platform>)" -ForEach $TestTa
449466
$script:HangEvent.platform | Should -Be 'native'
450467
}
451468

452-
It "Should have exception with App Hanging type" {
469+
It "Should have exception with expected type and value" {
453470
$script:HangEvent.exception | Should -Not -BeNullOrEmpty
454471
$script:HangEvent.exception.values | Should -Not -BeNullOrEmpty
455472
$exception = $script:HangEvent.exception.values[0]
456-
$exception.type | Should -Be 'App Hanging'
457-
$exception.value | Should -Be 'Application not responding'
473+
$exception.type | Should -Be $ExceptionType
474+
$exception.value | Should -Match $ExceptionValuePattern
458475
}
459476

460477
It "Should have AppHang mechanism" {
@@ -474,7 +491,7 @@ Describe "Sentry Unreal Desktop Integration Tests (<Platform>)" -ForEach $TestTa
474491
($tags | Where-Object { $_.key -eq 'test.suite' }).value | Should -Be 'integration'
475492
}
476493

477-
It "Should have CrashType tag" {
494+
It "Should have CrashType tag" -Skip:(-not $ExpectsCrashTypeTag) {
478495
$tags = $script:HangEvent.tags
479496
($tags | Where-Object { $_.key -eq 'CrashType' }).value | Should -Be 'Hang'
480497
}

plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,11 @@ bool FAndroidSentrySubsystem::IsHangTrackingSupported() const
342342
return false;
343343
}
344344

345+
bool FAndroidSentrySubsystem::IsNativeHangTrackingEnabled() const
346+
{
347+
return false;
348+
}
349+
345350
void FAndroidSentrySubsystem::CaptureFeedback(TSharedPtr<ISentryFeedback> feedback)
346351
{
347352
TSharedPtr<FAndroidSentryFeedback> feedbackAndroid = StaticCastSharedPtr<FAndroidSentryFeedback>(feedback);

plugin-dev/Source/Sentry/Private/Android/AndroidSentrySubsystem.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class FAndroidSentrySubsystem : public ISentrySubsystem
3232
virtual TSharedPtr<ISentryId> CaptureEnsure(const FString& type, const FString& message) override;
3333
virtual TSharedPtr<ISentryId> CaptureHang(uint32 HungThreadId) override;
3434
virtual bool IsHangTrackingSupported() const override;
35+
virtual bool IsNativeHangTrackingEnabled() const override;
3536
virtual void CaptureFeedback(TSharedPtr<ISentryFeedback> feedback) override;
3637
virtual void SetUser(TSharedPtr<ISentryUser> user) override;
3738
virtual void RemoveUser() override;

plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,11 @@ bool FAppleSentrySubsystem::IsHangTrackingSupported() const
463463
return false;
464464
}
465465

466+
bool FAppleSentrySubsystem::IsNativeHangTrackingEnabled() const
467+
{
468+
return false;
469+
}
470+
466471
void FAppleSentrySubsystem::CaptureFeedback(TSharedPtr<ISentryFeedback> feedback)
467472
{
468473
TSharedPtr<FAppleSentryFeedback> feedbackApple = StaticCastSharedPtr<FAppleSentryFeedback>(feedback);

plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class FAppleSentrySubsystem : public ISentrySubsystem
3131
virtual TSharedPtr<ISentryId> CaptureEnsure(const FString& type, const FString& message) override;
3232
virtual TSharedPtr<ISentryId> CaptureHang(uint32 HungThreadId) override;
3333
virtual bool IsHangTrackingSupported() const override;
34+
virtual bool IsNativeHangTrackingEnabled() const override;
3435
virtual void CaptureFeedback(TSharedPtr<ISentryFeedback> feedback) override;
3536
virtual void SetUser(TSharedPtr<ISentryUser> user) override;
3637
virtual void RemoveUser() override;

plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentryEvent.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,10 @@ bool FGenericPlatformSentryEvent::IsAnr() const
314314
return false;
315315
}
316316

317+
// The engine FThreadHeartBeat watcher reports "App Hanging" (see CaptureHang) while sentry-native's
318+
// app-hang watchdog reports "AppHang" - accept either so both detection mechanisms are recognized
317319
const char* type = sentry_value_as_string(sentry_value_get_by_key(firstException, "type"));
318-
bool isAppHangException = type && FCStringAnsi::Strcmp(type, "App Hanging") == 0;
320+
bool isAppHangException = type && (FCStringAnsi::Strcmp(type, "App Hanging") == 0 || FCStringAnsi::Strcmp(type, "AppHang") == 0);
319321

320322
sentry_value_t mechanism = sentry_value_get_by_key(firstException, "mechanism");
321323
const char* mechanismType = sentry_value_as_string(sentry_value_get_by_key(mechanism, "type"));

plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ FGenericPlatformSentrySubsystem::FGenericPlatformSentrySubsystem()
483483
, isScreenshotAttachmentEnabled(false)
484484
, isSessionReplayAttachmentEnabled(false)
485485
, isGpuDumpAttachmentEnabled(false)
486+
, bNativeHangTracking(false)
486487
, initTimestamp(FDateTime::UtcNow())
487488
{
488489
}
@@ -560,6 +561,15 @@ void FGenericPlatformSentrySubsystem::InitWithSettings(const USentrySettings* se
560561
ConfigureNetworkConnectFunc(options);
561562
ConfigureStackCaptureStrategy(options);
562563

564+
#if PLATFORM_WINDOWS || PLATFORM_MAC || PLATFORM_LINUX
565+
bNativeHangTracking = settings->EnableHangTracking && settings->UseNativeHangTracking;
566+
if (bNativeHangTracking)
567+
{
568+
sentry_options_set_enable_app_hang_tracking(options, 1);
569+
sentry_options_set_app_hang_timeout(options, static_cast<uint64_t>(settings->HangTimeoutDuration * 1000.0));
570+
}
571+
#endif
572+
563573
if (settings->EnableExternalCrashReporter)
564574
{
565575
ConfigureCrashReporterPath(options);
@@ -621,6 +631,16 @@ void FGenericPlatformSentrySubsystem::InitWithSettings(const USentrySettings* se
621631
isStackTraceEnabled = settings->AttachStacktrace;
622632
isPiiAttachmentEnabled = settings->SendDefaultPii;
623633

634+
if (bNativeHangTracking && isEnabled)
635+
{
636+
// OnEndFrame is broadcast on the game thread, so the first invocation latches it as the monitored
637+
// thread and every subsequent frame refreshes the heartbeat the daemon watches for staleness
638+
AppHangHeartbeatHandle = FCoreDelegates::OnEndFrame.AddLambda([]()
639+
{
640+
sentry_app_hang_heartbeat();
641+
});
642+
}
643+
624644
// Best-effort at writing user consent to disk so that user consent can change at runtime and persist
625645
// We should never have a valid user consent state return "Unknown", so assume that no consent value is written if we see this
626646
if (settings->bRequireUserConsent && GetUserConsent() == EUserConsent::Unknown)
@@ -657,6 +677,12 @@ void FGenericPlatformSentrySubsystem::InitWithSettings(const USentrySettings* se
657677

658678
void FGenericPlatformSentrySubsystem::Close()
659679
{
680+
if (AppHangHeartbeatHandle.IsValid())
681+
{
682+
FCoreDelegates::OnEndFrame.Remove(AppHangHeartbeatHandle);
683+
AppHangHeartbeatHandle.Reset();
684+
}
685+
660686
isEnabled = false;
661687

662688
#ifdef USE_SENTRY_SESSION_REPLAY

plugin-dev/Source/Sentry/Private/GenericPlatform/GenericPlatformSentrySubsystem.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class FGenericPlatformSentrySubsystem : public ISentrySubsystem
7171

7272
virtual void HandleAssert() override {}
7373
virtual bool IsHangTrackingSupported() const override { return false; }
74+
virtual bool IsNativeHangTrackingEnabled() const override { return bNativeHangTracking; }
7475
virtual FString GetDeviceType() const override { return TEXT("Desktop"); }
7576

7677
USentryBeforeSendHandler* GetBeforeSendHandler() const;
@@ -154,6 +155,10 @@ class FGenericPlatformSentrySubsystem : public ISentrySubsystem
154155
bool isSessionReplayAttachmentEnabled;
155156
bool isGpuDumpAttachmentEnabled;
156157

158+
bool bNativeHangTracking;
159+
160+
FDelegateHandle AppHangHeartbeatHandle;
161+
157162
FDateTime initTimestamp;
158163

159164
FString databaseParentPath;

plugin-dev/Source/Sentry/Private/Interface/SentrySubsystemInterface.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,6 @@ class ISentrySubsystem
7777
/** Unreal-specific methods that are not part of the platform's Sentry SDK API */
7878
virtual void HandleAssert() = 0;
7979
virtual bool IsHangTrackingSupported() const = 0;
80+
virtual bool IsNativeHangTrackingEnabled() const = 0;
8081
virtual FString GetDeviceType() const = 0;
8182
};

0 commit comments

Comments
 (0)