Skip to content

Commit

Permalink
feat: AppHangsV2 (#4379)
Browse files Browse the repository at this point in the history
Expose the AppHangsV2 algorithm with the options
enableAppHangTrackingV2 and
enableReportNonFullyBlockingAppHangs.

Fixes GH-3492
  • Loading branch information
philipphofmann authored Oct 8, 2024
1 parent 2d2068d commit 187edbf
Show file tree
Hide file tree
Showing 17 changed files with 317 additions and 76 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Added breadcrumb.origin private field (#4358)
- Custom redact modifier for SwiftUI (#4362)
- AppHangV2 detection (#4379) Add a new algorithm for detecting app hangs that differentiates between fully blocking and non-fully blocking app hangs. Read more in-depth in our [docs](https://docs.sentry.io/platforms/apple/guides/ios/configuration/app-hangs/#app-hangs-v2).
- Add support for arm64e (#3398)
- Add mergeable libraries support to dynamic libraries (#4381)

Expand Down
4 changes: 4 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
621AE74D2C626C510012E730 /* SentryANRTrackerV2.m in Sources */ = {isa = PBXBuildFile; fileRef = 621AE74C2C626C510012E730 /* SentryANRTrackerV2.m */; };
621D9F2F2B9B0320003D94DE /* SentryCurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */; };
621F61F12BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */; };
6221BBCA2CAA932100C627CA /* SentryANRType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6221BBC92CAA932100C627CA /* SentryANRType.swift */; };
62262B862BA1C46D004DA3DD /* SentryStatsdClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 62262B852BA1C46D004DA3DD /* SentryStatsdClient.h */; };
62262B882BA1C490004DA3DD /* SentryStatsdClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 62262B872BA1C490004DA3DD /* SentryStatsdClient.m */; };
62262B8B2BA1C4C1004DA3DD /* EncodeMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62262B8A2BA1C4C1004DA3DD /* EncodeMetrics.swift */; };
Expand Down Expand Up @@ -1088,6 +1089,7 @@
621AE74E2C626CF70012E730 /* SentryANRTrackerV2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTrackerV2Tests.swift; sourceTree = "<group>"; };
621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCurrentDateProvider.swift; sourceTree = "<group>"; };
621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnabledFeaturesBuilder.swift; sourceTree = "<group>"; };
6221BBC92CAA932100C627CA /* SentryANRType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRType.swift; sourceTree = "<group>"; };
62262B852BA1C46D004DA3DD /* SentryStatsdClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryStatsdClient.h; path = include/SentryStatsdClient.h; sourceTree = "<group>"; };
62262B872BA1C490004DA3DD /* SentryStatsdClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryStatsdClient.m; sourceTree = "<group>"; };
62262B8A2BA1C4C1004DA3DD /* EncodeMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodeMetrics.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2199,6 +2201,7 @@
children = (
6294774B2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift */,
62FC18AE2C9D5FAC008803CD /* SentryANRTracker.swift */,
6221BBC92CAA932100C627CA /* SentryANRType.swift */,
);
path = ANR;
sourceTree = "<group>";
Expand Down Expand Up @@ -4634,6 +4637,7 @@
D8ACE3C92762187200F5A213 /* SentryFileIOTrackingIntegration.m in Sources */,
63FE713B20DA4C1100CDBAE8 /* SentryCrashFileUtils.c in Sources */,
63FE716920DA4C1100CDBAE8 /* SentryCrashStackCursor.c in Sources */,
6221BBCA2CAA932100C627CA /* SentryANRType.swift in Sources */,
7BA61CCA247D128B00C130A8 /* SentryThreadInspector.m in Sources */,
D8CA12952C203E71005894F4 /* SentrySessionListener.swift in Sources */,
63FE718D20DA4C1100CDBAE8 /* SentryCrashReportStore.c in Sources */,
Expand Down
34 changes: 34 additions & 0 deletions Sources/Sentry/Public/SentryOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,40 @@ NS_SWIFT_NAME(Options)
*/
@property (nonatomic, assign) BOOL enableAppHangTracking;

#if SENTRY_UIKIT_AVAILABLE

/**
* AppHangTrackingV2 can differentiate between fully-blocking and non-fully blocking app hangs.
* fully-blocking app hang is when the main thread is stuck completely, and the app can't render a
* single frame. A non-fully-blocking app hang is when the app appears stuck to the user but can
still
* render a few frames. Fully-blocking app hangs are more actionable because the stacktrace shows
the
* exact blocking location on the main thread. As the main thread isn't completely blocked,
* non-fully-blocking app hangs can have a stacktrace that doesn't highlight the exact blocking
* location.
*
* You can use @c enableReportNonFullyBlockingAppHangs to ignore non-fully-blocking app hangs.
*
* @note This flag wins over enableAppHangTracking. When enabling both enableAppHangTracking and
enableAppHangTrackingV2, the SDK only enables enableAppHangTrackingV2 and disables
enableAppHangTracking.
*
* @warning This is an experimental feature and may still have bugs.
*/
@property (nonatomic, assign) BOOL enableAppHangTrackingV2;

/**
* When enabled the SDK reports non-fully-blocking app hangs. A non-fully-blocking app hang is when
* the app appears stuck to the user but can still render a few frames. For more information see @c
* enableAppHangTrackingV2.
*
* @note The default is @c YES. This feature only works when @c enableAppHangTrackingV2 is enabled.
*/
@property (nonatomic, assign) BOOL enableReportNonFullyBlockingAppHangs;

#endif // SENTRY_UIKIT_AVAILABLE

/**
* The minimum amount of time an app should be unresponsive to be classified as an App Hanging.
* @note The actual amount may be a little longer.
Expand Down
21 changes: 18 additions & 3 deletions Sources/Sentry/SentryANRTrackingIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ @interface SentryANRTrackingIntegration ()
@property (nonatomic, strong) id<SentryANRTracker> tracker;
@property (nonatomic, strong) SentryOptions *options;
@property (atomic, assign) BOOL reportAppHangs;
@property (atomic, assign) BOOL enableReportNonFullyBlockingAppHangs;

@end

Expand All @@ -40,9 +41,15 @@ - (BOOL)installWithOptions:(SentryOptions *)options
return NO;
}

#if SENTRY_HAS_UIKIT
self.tracker =
[SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval
isV2Enabled:options.enableAppHangTrackingV2];
#else
self.tracker =
[SentryDependencyContainer.sharedInstance getANRTrackerV1:options.appHangTimeoutInterval];
[SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval];

#endif // SENTRY_HAS_UIKIT
[self.tracker addListener:self];
self.options = options;
self.reportAppHangs = YES;
Expand Down Expand Up @@ -83,6 +90,12 @@ - (void)anrDetectedWithType:(enum SentryANRType)type
}

#if SENTRY_HAS_UIKIT
if (type == SentryANRTypeNonFullyBlocking
&& !self.options.enableReportNonFullyBlockingAppHangs) {
SENTRY_LOG_DEBUG(@"Ignoring non fully blocking app hang.")
return;
}

// If the app is not active, the main thread may be blocked or too busy.
// Since there is no UI for the user to interact, there is no need to report app hang.
if (SentryDependencyContainer.sharedInstance.application.applicationState
Expand All @@ -103,8 +116,10 @@ - (void)anrDetectedWithType:(enum SentryANRType)type
NSString *message = [NSString stringWithFormat:@"App hanging for at least %li ms.",
(long)(self.options.appHangTimeoutInterval * 1000)];
SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError];
SentryException *sentryException =
[[SentryException alloc] initWithValue:message type:SentryANRExceptionType];

NSString *exceptionType = [SentryAppHangTypeMapper getExceptionTypeWithAnrType:type];
SentryException *sentryException = [[SentryException alloc] initWithValue:message
type:exceptionType];

sentryException.mechanism = [[SentryMechanism alloc] initWithType:@"AppHang"];
sentryException.stacktrace = [threads[0] stacktrace];
Expand Down
19 changes: 7 additions & 12 deletions Sources/Sentry/SentryBaseIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,17 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options
#endif

if (integrationOptions & kIntegrationOptionEnableAppHangTracking) {
if (!options.enableAppHangTracking) {
[self logWithOptionName:@"enableAppHangTracking"];
return NO;
}

if (options.appHangTimeoutInterval == 0) {
[self logWithReason:@"because appHangTimeoutInterval is 0"];
#if SENTRY_HAS_UIKIT
if (!options.enableAppHangTracking && !options.enableAppHangTrackingV2) {
[self logWithOptionName:@"enableAppHangTracking && enableAppHangTrackingV2"];
return NO;
}
}

if (integrationOptions & kIntegrationOptionEnableAppHangTrackingV2) {
if (!options.enableAppHangTrackingV2) {
[self logWithOptionName:@"enableAppHangTrackingV2"];
#else
if (!options.enableAppHangTracking) {
[self logWithOptionName:@"enableAppHangTracking"];
return NO;
}
#endif // SENTRY_HAS_UIKIT

if (options.appHangTimeoutInterval == 0) {
[self logWithReason:@"because appHangTimeoutInterval is 0"];
Expand Down
71 changes: 39 additions & 32 deletions Sources/Sentry/SentryDependencyContainer.m
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#import "SentryANRTrackerV1.h"
#import "SentryANRTrackerV2.h"

#import "SentryBinaryImageCache.h"
#import "SentryDispatchFactory.h"
#import "SentryDispatchQueueWrapper.h"
Expand Down Expand Up @@ -32,6 +32,7 @@
#import <SentryTracer.h>

#if SENTRY_HAS_UIKIT
# import "SentryANRTrackerV2.h"
# import "SentryFramesTracker.h"
# import "SentryUIApplication.h"
# import <SentryScreenshot.h>
Expand All @@ -46,6 +47,12 @@
# import "SentryReachability.h"
#endif // !TARGET_OS_WATCH

@interface SentryDependencyContainer ()

@property (nonatomic, strong) id<SentryANRTracker> anrTracker;

@end

@implementation SentryDependencyContainer

static SentryDependencyContainer *instance;
Expand Down Expand Up @@ -301,32 +308,6 @@ - (SentryFramesTracker *)framesTracker SENTRY_DISABLE_THREAD_SANITIZER(
# endif // SENTRY_HAS_UIKIT
}

- (SentryANRTrackerV2 *)getANRTrackerV2:(NSTimeInterval)timeout
SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false alarms")
{
# if SENTRY_HAS_UIKIT
if (_anrTrackerV2 == nil) {
@synchronized(sentryDependencyContainerLock) {
if (_anrTrackerV2 == nil) {
_anrTrackerV2 =
[[SentryANRTrackerV2 alloc] initWithTimeoutInterval:timeout
crashWrapper:self.crashWrapper
dispatchQueueWrapper:self.dispatchQueueWrapper
threadWrapper:self.threadWrapper
framesTracker:self.framesTracker];
}
}
}

return _anrTrackerV2;
# else
SENTRY_LOG_DEBUG(
@"SentryDependencyContainer.getANRTrackerV2 only works with UIKit enabled. Ensure you're "
@"using the right configuration of Sentry that links UIKit.");
return nil;
# endif // SENTRY_HAS_UIKIT
}

- (SentrySwizzleWrapper *)swizzleWrapper SENTRY_DISABLE_THREAD_SANITIZER(
"double-checked lock produce false alarms")
{
Expand All @@ -348,13 +329,13 @@ - (SentrySwizzleWrapper *)swizzleWrapper SENTRY_DISABLE_THREAD_SANITIZER(
}
#endif // SENTRY_UIKIT_AVAILABLE

- (SentryANRTrackerV1 *)getANRTrackerV1:(NSTimeInterval)timeout
- (id<SentryANRTracker>)getANRTracker:(NSTimeInterval)timeout
SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false alarms")
{
if (_anrTrackerV1 == nil) {
if (_anrTracker == nil) {
@synchronized(sentryDependencyContainerLock) {
if (_anrTrackerV1 == nil) {
_anrTrackerV1 =
if (_anrTracker == nil) {
_anrTracker =
[[SentryANRTrackerV1 alloc] initWithTimeoutInterval:timeout
crashWrapper:self.crashWrapper
dispatchQueueWrapper:self.dispatchQueueWrapper
Expand All @@ -363,8 +344,34 @@ - (SentryANRTrackerV1 *)getANRTrackerV1:(NSTimeInterval)timeout
}
}

return _anrTrackerV1;
return _anrTracker;
}

#if SENTRY_HAS_UIKIT
- (id<SentryANRTracker>)getANRTracker:(NSTimeInterval)timeout
isV2Enabled:(BOOL)isV2Enabled
SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false alarms")
{
if (isV2Enabled) {
if (_anrTracker == nil) {
@synchronized(sentryDependencyContainerLock) {
if (_anrTracker == nil) {
_anrTracker = [[SentryANRTrackerV2 alloc]
initWithTimeoutInterval:timeout
crashWrapper:self.crashWrapper
dispatchQueueWrapper:self.dispatchQueueWrapper
threadWrapper:self.threadWrapper
framesTracker:self.framesTracker];
}
}
}

return _anrTracker;
} else {
return [self getANRTracker:timeout];
}
}
#endif // SENTRY_HAS_UIKIT

- (SentryNSProcessInfoWrapper *)processInfoWrapper SENTRY_DISABLE_THREAD_SANITIZER(
"double-checked lock produce false alarms")
Expand Down
3 changes: 2 additions & 1 deletion Sources/Sentry/SentryEvent.m
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ - (BOOL)isMetricKitEvent
- (BOOL)isAppHangEvent
{
return self.exceptions.count == 1 &&
[self.exceptions.firstObject.type isEqualToString:SentryANRExceptionType];
[SentryAppHangTypeMapper
isExceptionTypeAppHangWithExceptionType:self.exceptions.firstObject.type];
}

@end
Expand Down
12 changes: 8 additions & 4 deletions Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,11 @@ - (instancetype)init
self.enableUserInteractionTracing = YES;
self.idleTimeout = SentryTracerDefaultTimeout;
self.enablePreWarmedAppStartTracing = NO;
self.enableAppHangTrackingV2 = NO;
self.enableReportNonFullyBlockingAppHangs = YES;
#endif // SENTRY_HAS_UIKIT
self.enableAppHangTracking = YES;
self.appHangTimeoutInterval = 2.0;
self.enableAppHangTrackingV2 = NO;
self.enableAutoBreadcrumbTracking = YES;
self.enableNetworkTracking = YES;
self.enableFileIOTracing = YES;
Expand Down Expand Up @@ -435,6 +436,12 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
[self setBool:options[@"enablePreWarmedAppStartTracing"]
block:^(BOOL value) { self->_enablePreWarmedAppStartTracing = value; }];

[self setBool:options[@"enableAppHangTrackingV2"]
block:^(BOOL value) { self->_enableAppHangTrackingV2 = value; }];

[self setBool:options[@"enableReportNonFullyBlockingAppHangs"]
block:^(BOOL value) { self->_enableReportNonFullyBlockingAppHangs = value; }];

#endif // SENTRY_HAS_UIKIT

[self setBool:options[@"enableAppHangTracking"]
Expand All @@ -444,9 +451,6 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
self.appHangTimeoutInterval = [options[@"appHangTimeoutInterval"] doubleValue];
}

[self setBool:options[@"enableAppHangTrackingV2"]
block:^(BOOL value) { self->_enableAppHangTrackingV2 = value; }];

[self setBool:options[@"enableNetworkTracking"]
block:^(BOOL value) { self->_enableNetworkTracking = value; }];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ - (BOOL)installWithOptions:(SentryOptions *)options
[self.tracker start];

self.anrTracker =
[SentryDependencyContainer.sharedInstance getANRTrackerV1:options.appHangTimeoutInterval];
[SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval
isV2Enabled:options.enableAppHangTrackingV2];
[self.anrTracker addListener:self];

self.appStateManager = appStateManager;
Expand Down
13 changes: 5 additions & 8 deletions Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#import "SentryDefines.h"

@class SentryANRTrackerV1;
@class SentryANRTrackerV2;
@protocol SentryANRTracker;
@class SentryAppStateManager;
@class SentryBinaryImageCache;
@class SentryCrash;
Expand Down Expand Up @@ -63,8 +62,6 @@ SENTRY_NO_INIT
@property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueueWrapper;
@property (nonatomic, strong) SentryNSNotificationCenterWrapper *notificationCenterWrapper;
@property (nonatomic, strong) SentryDebugImageProvider *debugImageProvider;
@property (nonatomic, strong) SentryANRTrackerV1 *anrTrackerV1;
@property (nonatomic, strong) SentryANRTrackerV2 *anrTrackerV2;
@property (nonatomic, strong) SentryNSProcessInfoWrapper *processInfoWrapper;
@property (nonatomic, strong) SentrySystemWrapper *systemWrapper;
@property (nonatomic, strong) SentryDispatchFactory *dispatchFactory;
Expand All @@ -90,10 +87,10 @@ SENTRY_NO_INIT
@property (nonatomic, strong) SentryReachability *reachability;
#endif // !TARGET_OS_WATCH

- (SentryANRTrackerV1 *)getANRTrackerV1:(NSTimeInterval)timeout;
#if SENTRY_UIKIT_AVAILABLE
- (SentryANRTrackerV2 *)getANRTrackerV2:(NSTimeInterval)timeout;
#endif // SENTRY_UIKIT_AVAILABLE
- (id<SentryANRTracker>)getANRTracker:(NSTimeInterval)timeout;
#if SENTRY_HAS_UIKIT
- (id<SentryANRTracker>)getANRTracker:(NSTimeInterval)timeout isV2Enabled:(BOOL)isV2Enabled;
#endif // SENTRY_HAS_UIKIT

#if SENTRY_HAS_METRIC_KIT
@property (nonatomic, strong) SentryMXManager *metricKitManager API_AVAILABLE(
Expand Down
1 change: 0 additions & 1 deletion Sources/Sentry/include/SentryBaseIntegration.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ typedef NS_OPTIONS(NSUInteger, SentryIntegrationOption) {
kIntegrationOptionEnableCrashHandler = 1 << 16,
kIntegrationOptionEnableMetricKit = 1 << 17,
kIntegrationOptionEnableReplay = 1 << 18,
kIntegrationOptionEnableAppHangTrackingV2 = 1 << 19,
};

@class SentryOptions;
Expand Down
2 changes: 0 additions & 2 deletions Sources/Sentry/include/SentryOptions+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ FOUNDATION_EXPORT NSString *const kSentryDefaultEnvironment;

SENTRY_EXTERN BOOL sentry_isValidSampleRate(NSNumber *sampleRate);

@property (nonatomic, assign) BOOL enableAppHangTrackingV2;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,3 @@ protocol SentryANRTrackerDelegate {
func anrDetected(type: SentryANRType)
func anrStopped()
}

@objc
enum SentryANRType: Int {
case fullyBlocking
case nonFullyBlocking
case unknown
}
Loading

0 comments on commit 187edbf

Please sign in to comment.