-
-
Notifications
You must be signed in to change notification settings - Fork 359
New runloop observer #5632
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
base: main
Are you sure you want to change the base?
New runloop observer #5632
Conversation
7e0b439
to
de85790
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5632 +/- ##
=============================================
- Coverage 86.550% 8.157% -78.394%
=============================================
Files 417 386 -31
Lines 35429 27595 -7834
Branches 15353 57 -15296
=============================================
- Hits 30664 2251 -28413
- Misses 4725 25344 +20619
+ Partials 40 0 -40
... and 407 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
de85790
to
0590db9
Compare
var maxFPS = 60.0 | ||
if #available(iOS 13.0, tvOS 13.0, *) { | ||
let window = UIApplication.shared.connectedScenes.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }.first { $0.isKeyWindow } | ||
maxFPS = Double(window?.screen.maximumFramesPerSecond ?? 60) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the display rate can change dynamically in a few cases like when displaying video. Not seeing any way to subscribe to that change though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I didn't see it either. I figured using maximum at least would mean we would detect any time slow code is preventing the app from running at fastest possible. I think other things can cause the OS to decide to slow it down like low power mode. But you probably want to still know when frames are going to slow even if the OS chooses not to render them all
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CADisplayLink docs actually mention this a bit https://developer.apple.com/documentation/quartzcore/cadisplaylink#Preferred-and-Actual-Frame-Rates.
You run into a risk of falsely reporting slow frames if the frame rate changes. That's why we calculate the actual frame rate here:
sentry-cocoa/Sources/Sentry/SentryFramesTracker.m
Lines 186 to 197 in 9a17767
// Calculate the actual frame rate as pointed out by the Apple docs: | |
// https://developer.apple.com/documentation/quartzcore/cadisplaylink?language=objc The actual | |
// frame rate can change at any time by setting preferredFramesPerSecond or due to ProMotion | |
// display, low power mode, critical thermal state, and accessibility settings. Therefore we | |
// need to check the frame rate for every callback. | |
// targetTimestamp is only available on iOS 10.0 and tvOS 10.0 and above. We use a fallback of | |
// 60 fps. | |
_currentFrameRate = 60; | |
if (self.displayLinkWrapper.targetTimestamp != self.displayLinkWrapper.timestamp) { | |
_currentFrameRate = (uint64_t)round( | |
(1 / (self.displayLinkWrapper.targetTimestamp - self.displayLinkWrapper.timestamp))); | |
} |
private func waitForHang(semaphore: DispatchSemaphore, started: TimeInterval, isStarting: Bool) { | ||
dispatchPrecondition(condition: .onQueue(queue)) | ||
|
||
let timeout = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int((expectedFrameDuration + thresholdForFrameStacktrace) * 1_000)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: for easier readability I'd expect thresholdForFrameStacktraceDurationMs = expectedFrameDuration *1.5 * 1000
on assignment instead of calculating down here.
threads?.forEach { $0.current = false } | ||
threads?[0].current = true | ||
} | ||
let duration = dateProvider.systemUptime() - started |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming this works fine for the first frame edge case?
} | ||
} | ||
|
||
private func continueHang(started: TimeInterval, isStarting: Bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this work if the app is backgrounded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've never seen a case where the runloop doesn't get to finish an iteration. Even if the app is backgrounded your code isn't suspended instantly (for example NSNotifications get called on the main thread). If the main thread was suspended and never got a chance to resume then we would use the event stored to disk and report a fatal exception
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, I was more thinking about whether the lastFrameTime
value could get out of sync at all and report a false positive since that seems like it might have happened with the current implementation, but I'm guessing you're right that the runloop is allowed to finish its iteration.
private var maxHangTime: TimeInterval? | ||
|
||
func start() { | ||
let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.afterWaiting.rawValue | CFRunLoopActivity.beforeSources.rawValue, true, CFIndex(INT_MAX)) { [weak self] _, activity in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One weird edge-case that ChatGPT claims is possible is re-entry into the current runloop via CFRunLoopPerformBlock
that could result in false negatives, I guess as part of keyboard/modal interactions? I'm not very familiar with this API though so not sure if that's a real or hallucination.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll test out keyboards specifically
WIP because this has no tests yet, just opening it to let CI run
#skip-changelog