Skip to content

Commit

Permalink
Merge 22333fd into 44332bf
Browse files Browse the repository at this point in the history
  • Loading branch information
philipphofmann authored Feb 11, 2025
2 parents 44332bf + 22333fd commit 3350356
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 12 deletions.
7 changes: 6 additions & 1 deletion Sources/Sentry/SentryANRTrackerV1.m
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,13 @@ - (void)ANRStopped
targets = [self.listeners allObjects];
}

// We don't measure the duration of app hangs for the V1 to minimize the scope, and we will
// replace V1 with V2 anyway.
NSString *errorMessage = [NSString
stringWithFormat:@"App hanging for at least %li ms.", (long)(self.timeoutInterval * 1000)];

for (id<SentryANRTrackerDelegate> target in targets) {
[target anrStopped];
[target anrStoppedWithErrorMessage:errorMessage];
}
}

Expand Down
27 changes: 24 additions & 3 deletions Sources/Sentry/SentryANRTrackerV2.m
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,15 @@ - (void)detectANRs

NSInteger reportThreshold = 5;
NSTimeInterval sleepInterval = self.timeoutInterval / reportThreshold;
uint64_t sleepIntervalInNanos = timeIntervalToNanoseconds(sleepInterval);
uint64_t timeoutIntervalInNanos = timeIntervalToNanoseconds(self.timeoutInterval);

uint64_t appHangStoppedInterval = timeIntervalToNanoseconds(sleepInterval * 2);
CFTimeInterval appHangStoppedFrameDelayThreshold
= nanosecondsToTimeInterval(appHangStoppedInterval) * 0.2;

uint64_t lastAppHangStoppedSystemTime = dateProvider.systemTime - timeoutIntervalInNanos;
uint64_t lastAppHangStartedSystemTime = 0;

// Canceling the thread can take up to sleepInterval.
while (YES) {
Expand Down Expand Up @@ -135,9 +137,21 @@ - (void)detectANRs
if (appHangStopped) {
SENTRY_LOG_DEBUG(@"App hang stopped.");

// As we check every sleepInterval if the app is hanging, the app could already be
// hanging for almost the sleepInterval until we detect it and it could already
// stopped hanging almost a sleepInterval until we again detect it's not.
uint64_t appHangDurationNanos
= timeoutIntervalInNanos + nowSystemTime - lastAppHangStartedSystemTime;
NSTimeInterval appHangDurationMinimum
= nanosecondsToTimeInterval(appHangDurationNanos - sleepIntervalInNanos);
NSTimeInterval appHangDurationMaximum
= nanosecondsToTimeInterval(appHangDurationNanos + sleepIntervalInNanos);

// The App Hang stopped, don't block the App Hangs thread or the main thread with
// calling ANRStopped listeners.
[self.dispatchQueueWrapper dispatchAsyncWithBlock:^{ [self ANRStopped]; }];
[self.dispatchQueueWrapper dispatchAsyncWithBlock:^{
[self ANRStopped:appHangDurationMinimum to:appHangDurationMaximum];
}];

lastAppHangStoppedSystemTime = dateProvider.systemTime;
reported = NO;
Expand Down Expand Up @@ -173,6 +187,7 @@ - (void)detectANRs
SENTRY_LOG_WARN(@"App Hang detected: fully-blocking.");

reported = YES;
lastAppHangStartedSystemTime = dateProvider.systemTime;
[self ANRDetected:SentryANRTypeFullyBlocking];
}

Expand All @@ -183,6 +198,7 @@ - (void)detectANRs
SENTRY_LOG_WARN(@"App Hang detected: non-fully-blocking.");

reported = YES;
lastAppHangStartedSystemTime = dateProvider.systemTime;
[self ANRDetected:SentryANRTypeNonFullyBlocking];
}
}
Expand All @@ -205,15 +221,20 @@ - (void)ANRDetected:(enum SentryANRType)type
}
}

- (void)ANRStopped
- (void)ANRStopped:(NSTimeInterval)hangDurationMinimum to:(NSTimeInterval)hangDurationMaximum
{
NSArray *targets;
@synchronized(self.listeners) {
targets = [self.listeners allObjects];
}

// We round to 0.1 seconds accuracy because we can't precicely measure the app hand duration.
NSString *errorMessage =
[NSString stringWithFormat:@"App hanging between %.1f and %.1f seconds.",
hangDurationMinimum, hangDurationMaximum];

for (id<SentryANRTrackerDelegate> target in targets) {
[target anrStopped];
[target anrStoppedWithErrorMessage:errorMessage];
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Sentry/SentryANRTrackingIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ - (void)anrDetectedWithType:(enum SentryANRType)type
[SentrySDK captureEvent:event];
}

- (void)anrStopped
- (void)anrStoppedWithErrorMessage:(NSString *)errorMessage
{
// We dont report when an ANR ends.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ - (void)anrDetectedWithType:(enum SentryANRType)type
updateAppState:^(SentryAppState *appState) { appState.isANROngoing = YES; }];
}

- (void)anrStopped
- (void)anrStoppedWithErrorMessage:(NSString *)errorMessage
{
[self.appStateManager
updateAppState:^(SentryAppState *appState) { appState.isANROngoing = NO; }];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import Foundation
@objc
protocol SentryANRTrackerDelegate {
func anrDetected(type: SentryANRType)
func anrStopped()

func anrStopped(errorMessage: String)
}
10 changes: 7 additions & 3 deletions Tests/SentryTests/Integrations/ANR/SentryANRTrackerV1Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import XCTest

#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
class SentryANRTrackerV1Tests: XCTestCase, SentryANRTrackerDelegate {

private var sut: SentryANRTracker!
private var fixture: Fixture!
private var anrDetectedExpectation: XCTestExpectation!
private var anrStoppedExpectation: XCTestExpectation!
private let waitTimeout: TimeInterval = 2.0
private var lastANRStoppedErrorMessage: String?

private class Fixture {
let timeoutInterval: TimeInterval = 5
Expand Down Expand Up @@ -109,6 +110,8 @@ class SentryANRTrackerV1Tests: XCTestCase, SentryANRTrackerDelegate {

wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout)
XCTAssertEqual(expectedANRStoppedInvocations, fixture.dispatchQueue.dispatchAsyncInvocations.count)

XCTAssertEqual("App hanging for at least 5000 ms.", lastANRStoppedErrorMessage)
}

func testAppSuspended_NoANR() {
Expand Down Expand Up @@ -213,7 +216,8 @@ class SentryANRTrackerV1Tests: XCTestCase, SentryANRTrackerDelegate {
anrDetectedExpectation.fulfill()
}

func anrStopped() {
func anrStopped(errorMessage: String) {
lastANRStoppedErrorMessage = errorMessage
anrStoppedExpectation.fulfill()
}

Expand All @@ -233,7 +237,7 @@ class SentryANRTrackerTestDelegate: NSObject, SentryANRTrackerDelegate {
anrStoppedExpectation.isInverted = true
}

func anrStopped() {
func anrStopped(errorMessage: String) {
anrStoppedExpectation.fulfill()
}

Expand Down
105 changes: 103 additions & 2 deletions Tests/SentryTests/Integrations/ANR/SentryANRTrackerV2Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import XCTest
class SentryANRTrackerV2Tests: XCTestCase {

private let waitTimeout: TimeInterval = 1.0
private let timeoutInterval: TimeInterval = 2
private var timeoutInterval: TimeInterval = 2

private func getSut() throws -> (SentryANRTracker, TestCurrentDateProvider, TestDisplayLinkWrapper, TestSentryCrashWrapper, SentryTestThreadWrapper, SentryFramesTracker) {

let currentDate = TestCurrentDateProvider()

let crashWrapper = TestSentryCrashWrapper.sharedInstance()
let dispatchQueue = TestSentryDispatchQueueWrapper()
let threadWrapper = SentryTestThreadWrapper()
Expand Down Expand Up @@ -65,6 +66,72 @@ class SentryANRTrackerV2Tests: XCTestCase {
}

wait(for: [listener.anrStoppedExpectation], timeout: waitTimeout)

try assertAppHangStoppedErrorMessage([
(2.2, 3.0),
(2.3, 3.1)
], listener.anrsStoppedErrorMessage.last)
}

func testFullyBlockingAppHangWithLargeTimeoutInterval_ReportsCorrectErrorMessage() throws {
timeoutInterval = 5.0
let (sut, currentDate, displayLinkWrapper, _, _, _) = try getSut()
defer { sut.clear() }

let listener = SentryANRTrackerV2TestDelegate()
sut.add(listener: listener)

// The app must hang for slightly over the timeoutInterval to report an app hang
var advanced = 0.0
while advanced < timeoutInterval + 0.1 {
advanced += 0.01
currentDate.advance(by: 0.01)
}

wait(for: [listener.anrDetectedExpectation], timeout: timeoutInterval)

renderNormalFramesToStopAppHang(displayLinkWrapper)

for _ in 0..<20 {
displayLinkWrapper.normalFrame()
}

wait(for: [listener.anrStoppedExpectation], timeout: waitTimeout)

try assertAppHangStoppedErrorMessage([
(5.6, 7.6)
], listener.anrsStoppedErrorMessage.last)
}

func testFullyBlockingAppHangWithSmallTimeoutInterval_ReportsCorrectErrorMessage() throws {
timeoutInterval = 0.5
let (sut, currentDate, displayLinkWrapper, _, _, _) = try getSut()
defer { sut.clear() }

let listener = SentryANRTrackerV2TestDelegate()
sut.add(listener: listener)

// The app must hang for slightly over the timeoutInterval to report an app hang
var advanced = 0.0
while advanced < timeoutInterval + 0.1 {
advanced += 0.01
currentDate.advance(by: 0.01)
}

wait(for: [listener.anrDetectedExpectation], timeout: timeoutInterval)

renderNormalFramesToStopAppHang(displayLinkWrapper)

for _ in 0..<20 {
displayLinkWrapper.normalFrame()
}

wait(for: [listener.anrStoppedExpectation], timeout: waitTimeout)

try assertAppHangStoppedErrorMessage([
(0.6, 0.8),
(0.7, 0.9)
], listener.anrsStoppedErrorMessage.last)
}

/// For a non fully blocking app hang at least one frame must be rendered during the hang.
Expand All @@ -88,6 +155,11 @@ class SentryANRTrackerV2Tests: XCTestCase {
renderNormalFramesToStopAppHang(displayLinkWrapper)

wait(for: [listener.anrStoppedExpectation], timeout: waitTimeout)

try assertAppHangStoppedErrorMessage([
(2.2, 3.0),
(2.3, 3.1)
], listener.anrsStoppedErrorMessage.last)
}

/// 3 frozen frames aren't enough for a non fully blocking app hang.
Expand Down Expand Up @@ -160,6 +232,10 @@ class SentryANRTrackerV2Tests: XCTestCase {
renderNormalFramesToStopAppHang(displayLinkWrapper)

wait(for: [listener.anrStoppedExpectation], timeout: waitTimeout)
try assertAppHangStoppedErrorMessage([
(4.2, 5.0),
(4.3, 5.1)
], listener.anrsStoppedErrorMessage.last)
}

/// Fully blocking app hang, app hang stops, again fully blocking app hang
Expand Down Expand Up @@ -235,6 +311,19 @@ class SentryANRTrackerV2Tests: XCTestCase {
SentryLog.withOutLogs {
wait(for: [firstListener.anrDetectedExpectation, firstListener.anrStoppedExpectation, thirdListener.anrStoppedExpectation, thirdListener.anrDetectedExpectation], timeout: waitTimeout)
}

try assertAppHangStoppedErrorMessage([
(2.2, 3.0),
(2.3, 3.1)
], firstListener.anrsStoppedErrorMessage.last)
try assertAppHangStoppedErrorMessage([
(2.2, 3.0),
(2.3, 3.1)
], secondListener.anrsStoppedErrorMessage.last)
try assertAppHangStoppedErrorMessage([
(2.2, 3.0),
(2.3, 3.1)
], thirdListener.anrsStoppedErrorMessage.last)
}

func testTwoListeners_FullyBlocking_ReportedToBothListeners() throws {
Expand Down Expand Up @@ -455,13 +544,24 @@ class SentryANRTrackerV2Tests: XCTestCase {
displayLinkWrapper.frameWith(delay: 1.0)
}

/// We use threading so the app hang duration can differ slightly
private func assertAppHangStoppedErrorMessage(_ allowedDurations: [(Double, Double)], _ actualErrorMessage: String?) throws {
let errorMessage = try XCTUnwrap(actualErrorMessage, "The error message is nil.")

let allowedDurations = allowedDurations.map { "App hanging between \($0.0) and \($0.1) seconds." }
XCTAssertTrue(
allowedDurations.contains(errorMessage),
"The expected error messages don't contain: \(errorMessage)")
}

}

class SentryANRTrackerV2TestDelegate: NSObject, SentryANRTrackerDelegate {

let anrDetectedExpectation = XCTestExpectation(description: "Test Delegate ANR Detection")
let anrStoppedExpectation = XCTestExpectation(description: "Test Delegate ANR Stopped")
let anrsDetected = Invocations<Sentry.SentryANRType>()
let anrsStoppedErrorMessage = Invocations<String>()

init(shouldANRBeDetected: Bool = true, shouldStoppedBeCalled: Bool = true) {
if !shouldANRBeDetected {
Expand All @@ -476,7 +576,8 @@ class SentryANRTrackerV2TestDelegate: NSObject, SentryANRTrackerDelegate {
anrStoppedExpectation.assertForOverFulfill = true
}

func anrStopped() {
func anrStopped(errorMessage: String) {
anrsStoppedErrorMessage.record(errorMessage)
anrStoppedExpectation.fulfill()
}

Expand Down

0 comments on commit 3350356

Please sign in to comment.