Skip to content

Commit 141bc83

Browse files
committed
Updates
1 parent 29567f3 commit 141bc83

File tree

4 files changed

+98
-41
lines changed

4 files changed

+98
-41
lines changed

Sources/Sentry/SentryDependencyContainer.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,8 @@ - (RunLoopObserverObjcBridge *)observer SENTRY_THREAD_SANITIZER_DOUBLE_CHECKED_L
263263
SENTRY_LAZY_INIT(_observer,
264264
[[RunLoopObserverObjcBridge alloc] initWithDateProvider:self.dateProvider
265265
threadInspector:self.threadInspector
266-
debugImageCache:self.debugImageProvider]);
266+
debugImageCache:self.debugImageProvider
267+
fileManager:self.fileManager]);
267268
}
268269

269270
- (id<SentryANRTracker>)getANRTracker:(NSTimeInterval)timeout

Sources/Sentry/SentryDependencyContainerSwiftHelper.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#import "SentryDependencyContainerSwiftHelper.h"
22
#import "SentryDependencyContainer.h"
3+
#import "SentrySDK+Private.h"
4+
#import "SentryScope+Private.h"
5+
#import "SentryScope.h"
36
#import "SentrySwift.h"
47
#import "SentryUIApplication.h"
58

@@ -19,4 +22,13 @@ + (void)dispatchSyncOnMainQueue:(void (^)(void))block
1922
[SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchSyncOnMainQueue:block];
2023
}
2124

25+
+ (void)applyScopeTo:(SentryEvent *)event
26+
{
27+
SentryScope *scope = [SentrySDK currentHub].scope;
28+
SentryOptions *options = SentrySDK.options;
29+
if (scope != nil && options != nil) {
30+
[scope applyToEvent:event maxBreadcrumb:options.maxBreadcrumbs];
31+
}
32+
}
33+
2234
@end

Sources/Sentry/include/SentryDependencyContainerSwiftHelper.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
# import <UIKit/UIKit.h>
66
#endif // SENTRY_HAS_UIKIT
77

8+
@class SentryScope;
9+
@class SentryOptions;
10+
811
NS_ASSUME_NONNULL_BEGIN
912

1013
// Some Swift code needs to access SentryDependencyContainer. To
@@ -21,6 +24,8 @@ NS_ASSUME_NONNULL_BEGIN
2124

2225
+ (void)dispatchSyncOnMainQueue:(void (^)(void))block;
2326

27+
+ (void)applyScopeTo:(SentryEvent *)event;
28+
2429
@end
2530

2631
NS_ASSUME_NONNULL_END

Sources/Swift/RunloopObserver.swift

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ final class RunLoopObserver {
88
private let dateProvider: SentryCurrentDateProvider
99
private let threadInspector: ThreadInspector
1010
private let debugImageCache: DebugImageCache
11+
private let fileManager: SentryFileManager
1112

1213
init(
1314
dateProvider: SentryCurrentDateProvider,
1415
threadInspector: ThreadInspector,
1516
debugImageCache: DebugImageCache,
17+
fileManager: SentryFileManager,
1618
minHangTime: TimeInterval) {
1719
self.dateProvider = dateProvider
1820
self.threadInspector = threadInspector
1921
self.debugImageCache = debugImageCache
22+
self.fileManager = fileManager
2023
self.lastFrameTime = 0
2124
self.minHangTime = minHangTime
2225
#if canImport(UIKit) && !SENTRY_NO_UIKIT
@@ -32,6 +35,7 @@ final class RunLoopObserver {
3235
#endif
3336
expectedFrameDuration = 1.0 / maxFPS
3437
thresholdForFrameStacktrace = expectedFrameDuration * 0.5
38+
// TODO: Check for stored app hang
3539
}
3640

3741
// This queue is used to detect main thread hangs, they need to be detected on a background thread
@@ -55,25 +59,16 @@ final class RunLoopObserver {
5559
let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.afterWaiting.rawValue | CFRunLoopActivity.beforeSources.rawValue, true, CFIndex(INT_MAX)) { [weak self] _, activity in
5660
guard let self else { return }
5761

62+
let started = updateFrameStatistics()
5863
switch activity {
5964
case .beforeWaiting:
60-
updateFrameStatistics()
6165
running = false
6266
case .afterWaiting, .beforeSources:
63-
updateFrameStatistics()
6467
semaphore = DispatchSemaphore(value: 0)
6568
running = true
66-
let timeout = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int((expectedFrameDuration + thresholdForFrameStacktrace) * 1_000))
6769
let localSemaphore = semaphore
6870
queue.async { [weak self] in
69-
let result = localSemaphore.wait(timeout: timeout)
70-
switch result {
71-
case .timedOut:
72-
print("[HANG] Timeout, hang detected")
73-
self?.hangStarted()
74-
case .success:
75-
break
76-
}
71+
self?.waitForHang(semaphore: localSemaphore, started: started, isStarting: true)
7772
}
7873
default:
7974
fatalError()
@@ -82,13 +77,10 @@ final class RunLoopObserver {
8277
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
8378
}
8479

85-
private func updateFrameStatistics() {
80+
private func updateFrameStatistics() -> Double {
8681
dispatchPrecondition(condition: .onQueue(.main))
8782

8883
let currentTime = dateProvider.systemUptime()
89-
defer {
90-
lastFrameTime = currentTime
91-
}
9284
// Only consider frames that were within 2x the minHangTime
9385
frameStatistics = frameStatistics.filter { $0.startTime > currentTime - minHangTime * 2 }
9486

@@ -104,10 +96,17 @@ final class RunLoopObserver {
10496
frameStatistics.append((startTime: lastFrameTime, delayTime: frameDelay))
10597
}
10698
let totalTime = frameStatistics.map({ $0.delayTime }).reduce(0, +)
99+
let type: SentryANRType = frameStatistics.count > 0 ? .nonFullyBlocking : .fullyBlocking
107100
if totalTime > minHangTime {
108101
print("[HANG] Hang detected \(totalTime)")
109-
maxHangTime = max(maxHangTime ?? 0, totalTime)
110-
// print("[HANG] Hang max \(maxHangTime ?? 0)")
102+
let maxTime = max(maxHangTime ?? 0, totalTime)
103+
maxHangTime = maxTime
104+
// Update on disk hang
105+
queue.async { [weak self] in
106+
guard let self, let threads = threads, !threads.isEmpty else { return }
107+
let event = makeEvent(duration: maxTime, threads: threads, type: type)
108+
fileManager.storeAppHang(event)
109+
}
111110
} else {
112111
if let maxHangTime {
113112
// The hang has ended
@@ -119,52 +118,88 @@ final class RunLoopObserver {
119118
// non fully blocking hang. Maybe we will eventually support something like
120119
// "scroll hitches" and report each time a frame is dropped rather than an
121120
// overal hang event with just one stacktrace.
122-
let type: SentryANRType = frameStatistics.count > 0 ? .nonFullyBlocking : .fullyBlocking
123121
queue.async { [weak self] in
124-
self?.recordHang(duration: maxHangTime, type: type)
122+
guard let self, let threads = threads, !threads.isEmpty else { return }
123+
let event = makeEvent(duration: maxHangTime, threads: threads, type: type)
124+
SentrySDK.capture(event: event)
125125
}
126126
}
127127
maxHangTime = nil
128128
}
129129
}
130+
lastFrameTime = currentTime
131+
return currentTime
130132
}
131133

132134
// MARK: Background queue
133-
135+
136+
private var blockingDuration: TimeInterval?
134137
private var threads: [SentryThread]?
135138

136-
private func hangStarted() {
139+
private func waitForHang(semaphore: DispatchSemaphore, started: TimeInterval, isStarting: Bool) {
137140
dispatchPrecondition(condition: .onQueue(queue))
138-
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()
141+
142+
let timeout = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int((expectedFrameDuration + thresholdForFrameStacktrace) * 1_000))
143+
let result = semaphore.wait(timeout: timeout)
144+
switch result {
145+
case .timedOut:
146+
semaphore.signal()
147+
print("[HANG] Timeout, hang detected")
148+
continueHang(started: started, isStarting: isStarting)
149+
waitForHang(semaphore: semaphore, started: started, isStarting: false)
150+
case .success:
151+
break
152+
}
142153
}
143154

144-
private func recordHang(duration: TimeInterval, type: SentryANRType) {
155+
// TODO: Only write hang if it's long enough
156+
// TODO: Need to clear hang details after the hang ends
157+
// Problem: If we are detecting a multiple runloop hang, which then turns into a single long hang
158+
// we might want to add the total time of that long hang to what is on disk from the multiple runloop hang
159+
// Or we could not do that and just say we only overwrite what is on disk if the hang exceeds the time
160+
// of the multiple runloop hang.
161+
// Could have two paths, fullyBlocking only used when the semaphore times out, we keep tracking in memory until
162+
// it exceeds the threshold then we write to disk.
163+
// Non fully blocking only writes when the runloop finishes if it exceeds the threshold.
164+
// Sampled stacktrace should be kept separate from time, because time for nonFullyBlocking is kep on main thread
165+
// time for fullyBlocking is kept on background thread
166+
167+
// TODO: Not using should sample
168+
private func continueHang(started: TimeInterval, isStarting: Bool) {
145169
dispatchPrecondition(condition: .onQueue(queue))
146-
147-
guard let threads, !threads.isEmpty else {
148-
return
170+
171+
if isStarting {
172+
// A hang lasts a while, but we only support showing the stacktrace when it was first detected
173+
threads = threadInspector.getCurrentThreadsWithStackTrace()
174+
threads?.forEach { $0.current = false }
175+
threads?[0].current = true
176+
}
177+
let duration = dateProvider.systemUptime() - started
178+
blockingDuration = duration
179+
if let threads, !threads.isEmpty, duration > minHangTime {
180+
// Hangs detected in the background are always fully blocking
181+
// Otherwise we'd be detecting them on the main thread.
182+
fileManager.storeAppHang(makeEvent(duration: duration, threads: threads, type: .fullyBlocking))
149183
}
150184

151-
let event = Event()
185+
}
186+
187+
// Safe to call from any thread
188+
private func makeEvent(duration: TimeInterval, threads: [SentryThread], type: SentryANRType) -> Event {
189+
var event = Event()
152190
SentryLevelBridge.setBreadcrumbLevelOn(event, level: SentryLevel.error.rawValue)
153191
let exceptionType = SentryAppHangTypeMapper.getExceptionType(anrType: type)
154192
let exception = Exception(value: String(format: "App hanging for %.3f seconds.", duration), type: exceptionType)
155193
let mechanism = Mechanism(type: "AppHang")
156194
exception.mechanism = mechanism
157195
exception.stacktrace = threads[0].stacktrace
158196
exception.stacktrace?.snapshot = true
159-
160-
threads.forEach { $0.current = false }
161-
threads[0].current = true
162-
197+
exception.stacktrace?.snapshot = true
163198
event.exceptions = [exception]
164199
event.threads = threads
165-
166200
event.debugMeta = debugImageCache.getDebugImagesFromCacheFor(threads: event.threads)
167-
SentrySDK.capture(event: event)
201+
SentryDependencyContainerSwiftHelper.applyScope(to: event)
202+
return event
168203
}
169204
}
170205

@@ -176,10 +211,14 @@ final class RunLoopObserver {
176211
@objc public init(
177212
dateProvider: SentryCurrentDateProvider,
178213
threadInspector: ThreadInspector,
179-
debugImageCache: DebugImageCache) {
180-
observer = RunLoopObserver(dateProvider: dateProvider,
181-
threadInspector: threadInspector,
182-
debugImageCache: debugImageCache, minHangTime: 2)
214+
debugImageCache: DebugImageCache,
215+
fileManager: SentryFileManager) {
216+
observer = RunLoopObserver(
217+
dateProvider: dateProvider,
218+
threadInspector: threadInspector,
219+
debugImageCache: debugImageCache,
220+
fileManager: fileManager,
221+
minHangTime: 2)
183222
}
184223

185224
@objc public func start() {

0 commit comments

Comments
 (0)