Skip to content

Commit 29567f3

Browse files
committed
Updates
1 parent a4768e3 commit 29567f3

File tree

5 files changed

+116
-90
lines changed

5 files changed

+116
-90
lines changed

Sources/Sentry/SentryDependencyContainer.m

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,21 +85,13 @@ @interface SentryDependencyContainer ()
8585

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

88-
@property (nonatomic, strong) SentryDependencyScope *scope;
89-
90-
@property (nonatomic, strong) RunLoopObserverObjcBridge *observer;
91-
9288
@end
9389

94-
@interface SentryDebugImageProviderWorkaround: NSObject <DebugImageCache>
90+
@interface SentryDebugImageProvider () <DebugImageCache>
9591

9692
@end
9793

98-
@implementation SentryDebugImageProviderWorkaround
99-
100-
- (NSArray<SentryDebugMeta *> * _Nonnull)getDebugImagesFromCacheFor:(NSArray<SentryThread *> * _Nullable)threads {
101-
return [SentryDependencyContainer.sharedInstance.debugImageProvider getDebugImagesFromCacheForThreads:threads];
102-
}
94+
@interface SentryThreadInspector () <ThreadInspector>
10395

10496
@end
10597

@@ -179,9 +171,7 @@ - (instancetype)init
179171
_random = [[SentryRandom alloc] init];
180172
_threadWrapper = [[SentryThreadWrapper alloc] init];
181173
_binaryImageCache = [[SentryBinaryImageCache alloc] init];
182-
_scope = [[SentryDependencyScope alloc] initWithOptions:SentrySDK.options debugImageCache:[[SentryDebugImageProviderWorkaround alloc] init]];
183-
_observer = [[RunLoopObserverObjcBridge alloc] initWithDependencies:_scope];
184-
_dateProvider = _scope.dateProvider;
174+
_dateProvider = [[SentryDefaultCurrentDateProvider alloc] init];
185175

186176
_notificationCenterWrapper = [NSNotificationCenter defaultCenter];
187177
#if SENTRY_HAS_UIKIT
@@ -268,6 +258,14 @@ - (SentryCrash *)crashReporter SENTRY_THREAD_SANITIZER_DOUBLE_CHECKED_LOCK
268258
[[SentryCrash alloc] initWithBasePath:SentrySDK.options.cacheDirectoryPath]);
269259
}
270260

261+
- (RunLoopObserverObjcBridge *)observer SENTRY_THREAD_SANITIZER_DOUBLE_CHECKED_LOCK
262+
{
263+
SENTRY_LAZY_INIT(_observer,
264+
[[RunLoopObserverObjcBridge alloc] initWithDateProvider:self.dateProvider
265+
threadInspector:self.threadInspector
266+
debugImageCache:self.debugImageProvider]);
267+
}
268+
271269
- (id<SentryANRTracker>)getANRTracker:(NSTimeInterval)timeout
272270
SENTRY_THREAD_SANITIZER_DOUBLE_CHECKED_LOCK
273271
{

Sources/Sentry/SentrySDK.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ + (void)startWithOptions:(SentryOptions *)options
217217
NSLog(@"[SENTRY] [WARNING] SentrySDK not started. Running from Xcode preview.");
218218
return;
219219
}
220+
[SentryDependencyContainer.sharedInstance.observer start];
220221

221222
[SentrySDKLogSupport configure:options.debug diagnosticLevel:options.diagnosticLevel];
222223

Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
@class SentryUIViewControllerPerformanceTracker;
4747
@class SentryWatchdogTerminationScopeObserver;
4848
@class SentryWatchdogTerminationAttributesProcessor;
49+
@class RunLoopObserverObjcBridge;
4950
@class SentryWatchdogTerminationBreadcrumbProcessor;
5051
#endif // SENTRY_UIKIT_AVAILABLE
5152

@@ -122,6 +123,8 @@ SENTRY_NO_INIT
122123
@property (nonatomic, strong) id<SentryDispatchQueueProviderProtocol> dispatchQueueProvider;
123124
@property (nonatomic, strong) SentryNSTimerFactory *timerFactory;
124125

126+
@property (nonatomic, strong) RunLoopObserverObjcBridge *observer;
127+
125128
@property (nonatomic, strong) SentrySwizzleWrapper *swizzleWrapper;
126129
#if SENTRY_UIKIT_AVAILABLE
127130
@property (nonatomic, strong) SentryFramesTracker *framesTracker;

Sources/Sentry/include/SentryPrivate.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@
2828
#import "SentrySession.h"
2929
#import "SentrySpanDataKey.h"
3030
#import "SentrySpanOperation.h"
31+
#import "SentryThreadInspector.h"
3132
#import "SentryTraceHeader.h"
3233
#import "SentryTraceOrigin.h"
33-
#import "SentryThreadInspector.h"

Sources/Swift/RunloopObserver.swift

Lines changed: 100 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,55 @@
11
@_implementationOnly import _SentryPrivate
2-
3-
protocol ThreadInspector {
4-
func getCurrentThreadsWithStackTrace() -> [SentryThread]
5-
}
6-
7-
@objc @_spi(Private) public protocol DebugImageCache {
8-
func getDebugImagesFromCache(for threads: [SentryThread]?) -> [DebugMeta]
9-
}
10-
11-
protocol ThreadInspectorProviding {
12-
var threadInspector: ThreadInspector { get }
13-
}
14-
15-
protocol SentryCurrentDateProviding {
16-
var dateProvider: SentryCurrentDateProvider { get }
17-
}
18-
19-
protocol DebugImageCacheProviding {
20-
var debugImageCache: DebugImageCache { get }
21-
}
22-
23-
typealias RunLoopObserverDependencies = SentryCurrentDateProviding & ThreadInspectorProviding & DebugImageCacheProviding
24-
25-
final class RunloopObserver {
26-
let dependencies: RunLoopObserverDependencies
27-
init(dependencies: RunLoopObserverDependencies, minHangTime: TimeInterval) {
28-
self.dependencies = dependencies
2+
#if canImport(UIKit) && !SENTRY_NO_UIKIT
3+
import UIKit
4+
#endif
5+
6+
final class RunLoopObserver {
7+
8+
private let dateProvider: SentryCurrentDateProvider
9+
private let threadInspector: ThreadInspector
10+
private let debugImageCache: DebugImageCache
11+
12+
init(
13+
dateProvider: SentryCurrentDateProvider,
14+
threadInspector: ThreadInspector,
15+
debugImageCache: DebugImageCache,
16+
minHangTime: TimeInterval) {
17+
self.dateProvider = dateProvider
18+
self.threadInspector = threadInspector
19+
self.debugImageCache = debugImageCache
2920
self.lastFrameTime = 0
3021
self.minHangTime = minHangTime
22+
#if canImport(UIKit) && !SENTRY_NO_UIKIT
23+
var maxFPS = 60.0
24+
if #available(iOS 13.0, *) {
25+
let window = UIApplication.shared.connectedScenes.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }.first { $0.isKeyWindow }
26+
maxFPS = Double(window?.screen.maximumFramesPerSecond ?? 60)
27+
} else {
28+
maxFPS = Double(UIScreen.main.maximumFramesPerSecond)
29+
}
30+
#else
31+
let maxFPS: Double = 60.0
32+
#endif
33+
expectedFrameDuration = 1.0 / maxFPS
34+
thresholdForFrameStacktrace = expectedFrameDuration * 0.5
3135
}
3236

3337
// This queue is used to detect main thread hangs, they need to be detected on a background thread
3438
// since the main thread is hanging.
35-
let queue = DispatchQueue(label: "io.sentry.runloop-observer-checker")
36-
var semaphore = DispatchSemaphore(value: 0)
37-
let minHangTime: TimeInterval
39+
private let queue = DispatchQueue(label: "io.sentry.runloop-observer-checker")
40+
private let minHangTime: TimeInterval
41+
private let expectedFrameDuration: TimeInterval
42+
private let thresholdForFrameStacktrace: TimeInterval
3843

3944
// MARK: Main queue
4045

41-
var lastFrameTime: TimeInterval
42-
var running = false
43-
var frameStatistics = [(startTime: TimeInterval, delayTime: TimeInterval)]()
46+
private var semaphore = DispatchSemaphore(value: 0)
47+
private var lastFrameTime: TimeInterval
48+
private var running = false
49+
private var frameStatistics = [(startTime: TimeInterval, delayTime: TimeInterval)]()
50+
// Keeps track of how long the current hang has been running for
51+
// Set to nil after the current hang ends
52+
private var maxHangTime: TimeInterval?
4453

4554
func start() {
4655
let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.afterWaiting.rawValue | CFRunLoopActivity.beforeSources.rawValue, true, CFIndex(INT_MAX)) { [weak self] _, activity in
@@ -54,7 +63,7 @@ final class RunloopObserver {
5463
updateFrameStatistics()
5564
semaphore = DispatchSemaphore(value: 0)
5665
running = true
57-
let timeout = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(minHangTime * 1_000))
66+
let timeout = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int((expectedFrameDuration + thresholdForFrameStacktrace) * 1_000))
5867
let localSemaphore = semaphore
5968
queue.async { [weak self] in
6069
let result = localSemaphore.wait(timeout: timeout)
@@ -66,18 +75,17 @@ final class RunloopObserver {
6675
break
6776
}
6877
}
69-
// print("[HANG] Woken up")
7078
default:
7179
fatalError()
7280
}
7381
}
7482
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
7583
}
7684

77-
func updateFrameStatistics() {
85+
private func updateFrameStatistics() {
7886
dispatchPrecondition(condition: .onQueue(.main))
7987

80-
let currentTime = dependencies.dateProvider.systemUptime()
88+
let currentTime = dateProvider.systemUptime()
8189
defer {
8290
lastFrameTime = currentTime
8391
}
@@ -86,41 +94,54 @@ final class RunloopObserver {
8694

8795
semaphore.signal()
8896
if running {
89-
let expectedFrameTime = lastFrameTime + 1.0 / 60.0
90-
let frameDelay = currentTime - expectedFrameTime
91-
if frameDelay > minHangTime {
92-
print("[HANG] Hang detected \(frameDelay)s")
93-
queue.async { [weak self] in
94-
self?.recordHang(duration: frameDelay)
95-
}
96-
frameStatistics.removeAll()
97-
} else if frameDelay > 0 {
97+
let frameDuration = currentTime - lastFrameTime
98+
let frameDelay = frameDuration - expectedFrameDuration
99+
// A hang is characterized by the % of a time period that the app is rendering late frames
100+
// We use 50% of `minHangTime * 2` as the threshold for reporting a hang.
101+
// Once this threshold is crossed, any frame that was > 50% late is considered a hanging frame.
102+
// If a single frames delay is > minHangTime, it is considered a "fullyBlocking" hang.
103+
if frameDelay > 0 {
98104
frameStatistics.append((startTime: lastFrameTime, delayTime: frameDelay))
99105
}
100106
let totalTime = frameStatistics.map({ $0.delayTime }).reduce(0, +)
101-
if totalTime > minHangTime * 0.99 {
102-
print("[HANG] Detected non-blocking hang")
103-
// TODO: Keep on recording until blocking period is over (or some max time)
104-
// TODO: Get stacktraces from when the individual blocking events occured
105-
// TODO: Send each event
107+
if totalTime > minHangTime {
108+
print("[HANG] Hang detected \(totalTime)")
109+
maxHangTime = max(maxHangTime ?? 0, totalTime)
110+
// print("[HANG] Hang max \(maxHangTime ?? 0)")
111+
} else {
112+
if let maxHangTime {
113+
// The hang has ended
114+
print("[HANG] Hang reporting \(maxHangTime)")
115+
// Note: A non fully blocking hang always has multiple stacktraces
116+
// because it is composed of multpile delayed frames. Each delayed frame has a stacktrace.
117+
// We only support sending one stacktrace per event so we take the most recent one.
118+
// Another option would be to generate one event for each delayed frame in the
119+
// non fully blocking hang. Maybe we will eventually support something like
120+
// "scroll hitches" and report each time a frame is dropped rather than an
121+
// overal hang event with just one stacktrace.
122+
let type: SentryANRType = frameStatistics.count > 0 ? .nonFullyBlocking : .fullyBlocking
123+
queue.async { [weak self] in
124+
self?.recordHang(duration: maxHangTime, type: type)
125+
}
126+
}
127+
maxHangTime = nil
106128
}
107129
}
108130
}
109131

110132
// MARK: Background queue
111133

112-
var threads: [SentryThread]?
134+
private var threads: [SentryThread]?
113135

114-
func hangStarted() {
136+
private func hangStarted() {
115137
dispatchPrecondition(condition: .onQueue(queue))
116138

117-
// TODO: Write to disk to record fatal hangs on app start
118-
119-
// Record threads at start of hang
120-
threads = dependencies.threadInspector.getCurrentThreadsWithStackTrace()
139+
// TOD: Write to disk to record fatal hangs on app start
140+
// Record threads when the hang is first detected
141+
threads = threadInspector.getCurrentThreadsWithStackTrace()
121142
}
122143

123-
func recordHang(duration: TimeInterval) {
144+
private func recordHang(duration: TimeInterval, type: SentryANRType) {
124145
dispatchPrecondition(condition: .onQueue(queue))
125146

126147
guard let threads, !threads.isEmpty else {
@@ -129,8 +150,8 @@ final class RunloopObserver {
129150

130151
let event = Event()
131152
SentryLevelBridge.setBreadcrumbLevelOn(event, level: SentryLevel.error.rawValue)
132-
let exceptionType = SentryAppHangTypeMapper.getExceptionType(anrType: .fullyBlocking)
133-
let exception = Exception(value: "App hanging for \(duration) seconds.", type: exceptionType)
153+
let exceptionType = SentryAppHangTypeMapper.getExceptionType(anrType: type)
154+
let exception = Exception(value: String(format: "App hanging for %.3f seconds.", duration), type: exceptionType)
134155
let mechanism = Mechanism(type: "AppHang")
135156
exception.mechanism = mechanism
136157
exception.stacktrace = threads[0].stacktrace
@@ -142,31 +163,34 @@ final class RunloopObserver {
142163
event.exceptions = [exception]
143164
event.threads = threads
144165

145-
event.debugMeta = dependencies.debugImageCache.getDebugImagesFromCache(for: event.threads)
166+
event.debugMeta = debugImageCache.getDebugImagesFromCacheFor(threads: event.threads)
146167
SentrySDK.capture(event: event)
147168
}
148169
}
149170

150171
@objc
151172
@_spi(Private) public final class RunLoopObserverObjcBridge: NSObject {
152-
@_spi(Private) @objc public init(dependencies: SentryDependencyScope) {
153-
observer = RunloopObserver(dependencies: dependencies, minHangTime: 2)
154-
observer.start()
173+
174+
private let observer: RunLoopObserver
175+
176+
@objc public init(
177+
dateProvider: SentryCurrentDateProvider,
178+
threadInspector: ThreadInspector,
179+
debugImageCache: DebugImageCache) {
180+
observer = RunLoopObserver(dateProvider: dateProvider,
181+
threadInspector: threadInspector,
182+
debugImageCache: debugImageCache, minHangTime: 2)
155183
}
156-
let observer: RunloopObserver
157184

158-
}
159-
160-
@objc
161-
@_spi(Private) public class SentryDependencyScope: NSObject, SentryCurrentDateProviding, DebugImageCacheProviding, ThreadInspectorProviding {
162-
@objc @_spi(Private) public init(options: Options, debugImageCache: DebugImageCache) {
163-
self.threadInspector = SentryThreadInspector(options: options)
164-
self.debugImageCache = debugImageCache
185+
@objc public func start() {
186+
observer.start()
165187
}
188+
}
166189

167-
@_spi(Private) @objc public let dateProvider: SentryCurrentDateProvider = SentryDefaultCurrentDateProvider()
168-
let threadInspector: ThreadInspector
169-
let debugImageCache: DebugImageCache
190+
@objc @_spi(Private) public protocol ThreadInspector {
191+
func getCurrentThreadsWithStackTrace() -> [SentryThread]
170192
}
171193

172-
extension SentryThreadInspector: ThreadInspector { }
194+
@objc @_spi(Private) public protocol DebugImageCache {
195+
func getDebugImagesFromCacheFor(threads: [SentryThread]?) -> [DebugMeta]
196+
}

0 commit comments

Comments
 (0)