Skip to content

Commit

Permalink
RUMM-1743 Add workaround to potential iOS 15 crash on `processInfo.is…
Browse files Browse the repository at this point in the history
…LowPowerModeEnabled`

We suspect an iOS 15 bug (ref.: https://openradar.appspot.com/FB9741207) which leads to rare
`_os_unfair_lock_recursive_abort` crash when `processInfo.isLowPowerModeEnabled` is accessed
directly in the notification handler. As a workaround, we defer its access to the next run loop
where underlying lock should be already released.

ref.: https://openradar.appspot.com/FB9741207
  • Loading branch information
ncreated committed Nov 5, 2021
1 parent 1326c84 commit 5db35ab
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 7 deletions.
18 changes: 12 additions & 6 deletions Sources/Datadog/Core/System/MobileDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,9 @@ internal class MobileDevice {
/// Observes "Low Power Mode" setting changes and provides `isLowPowerModeEnabled` value in a thread-safe manner.
///
/// Note: this was added in https://github.com/DataDog/dd-sdk-ios/issues/609 where `ProcessInfo.isLowPowerModeEnabled` was considered
/// not thread-safe on iOS 15. With this monitor, we change from pulling to push model for reading this property. Now, it will never be read simultaneously
/// by multiple SDK threads - instead it will be read only once after LPM setting change and bridged to other threads through thread-safe `ValuePublisher`.
///
/// This should mitigate the crash originating in our SDK. We can't however prevent other code (e.g. application code) from reading this value simultaneously
/// and causing a deadlock with SDK reads - ref. radar raised with Apple: FB9661108.
/// not thread-safe on iOS 15. We suspect a bug present in iOS 15, where accessing `processInfo.isLowPowerModeEnabled` within a pending
/// `.NSProcessInfoPowerStateDidChange` completion handler can sometimes lead to `_os_unfair_lock_recursive_abort` crash. The issue
/// was reported to Apple, ref.: https://openradar.appspot.com/FB9741207
private final class LowPowerModeMonitor {
var isLowPowerModeEnabled: Bool {
publisher.currentValue
Expand All @@ -137,7 +135,15 @@ private final class LowPowerModeMonitor {
guard let processInfo = notification.object as? ProcessInfo else {
return
}
self?.publisher.publishAsync(processInfo.isLowPowerModeEnabled)

// We suspect an iOS 15 bug (ref.: https://openradar.appspot.com/FB9741207) which leads to rare
// `_os_unfair_lock_recursive_abort` crash when `processInfo.isLowPowerModeEnabled` is accessed
// directly in the notification handler. As a workaround, we defer its access to the next run loop
// where underlying lock should be already released.
OperationQueue.main.addOperation {
let nextValue = processInfo.isLowPowerModeEnabled
self?.publisher.publishAsync(nextValue)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ class MobileDeviceTests: XCTestCase {
)

// Then
XCTAssertEqual(mobileDevice.currentBatteryStatus().isLowPowerModeEnabled, !isLowPowerModeEnabled)
let expectation = self.expectation(description: "Update `isLowPowerModeEnabled` in `BatteryStatus`")
wait(
until: { mobileDevice.currentBatteryStatus().isLowPowerModeEnabled == !isLowPowerModeEnabled },
andThenFulfill: expectation
)
waitForExpectations(timeout: 0.5, handler: nil)
}

func testWhenRunningOnMobile_itTogglesBatteryMonitoring() {
Expand Down
12 changes: 12 additions & 0 deletions Tests/DatadogTests/Helpers/XCTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ extension XCTestCase {
}
}

/// Waits until given `condition` returns `true` and then fulfills the `expectation`.
/// It executes `condition()` block on the main thread, in every run loop.
func wait(until condition: @escaping () -> Bool, andThenFulfill expectation: XCTestExpectation) {
if condition() {
expectation.fulfill()
} else {
OperationQueue.main.addOperation { [weak self] in
self?.wait(until: condition, andThenFulfill: expectation)
}
}
}

/// Asserts that two dictionaries are equal.
/// It uses debug string representation of values to check equality of `Any` values.
func AssertDictionariesEqual(
Expand Down

0 comments on commit 5db35ab

Please sign in to comment.