Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: AppHangsV2 #4379

Merged
merged 7 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 mergeable libraries support to dynamic libraries (#4381)

### Improvements
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
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
Loading