[iOS] Replace UIKit background task with a RunningBoard FinishTaskInterruptable assertion
https://bugs.webkit.org/show_bug.cgi?id=209825
<rdar://problem/61118503>

Reviewed by Geoffrey Garen.

Source/WebKit:

Replace UIKit background task with a RunningBoard FinishTaskInterruptable assertion on iOS.
Our UIProcess gets terminated too frequently when the UIKit background task expires when
the UIProcess holds it for longer than 30 seconds in the background. The RunningBoard
FinishTaskInterruptable assertion is supposed to be equivalent but would cause suspension
of our UIProcess on expiration, instead of termination.

* UIProcess/ios/ProcessAssertionIOS.mm:
(-[WKProcessAssertionBackgroundTaskManager init]):
(-[WKProcessAssertionBackgroundTaskManager _scheduleReleaseTask]):
(-[WKProcessAssertionBackgroundTaskManager _cancelPendingReleaseTask]):
(-[WKProcessAssertionBackgroundTaskManager _hasBackgroundTask]):
(-[WKProcessAssertionBackgroundTaskManager _updateBackgroundTask]):
(-[WKProcessAssertionBackgroundTaskManager assertionWillInvalidate:]):
(-[WKProcessAssertionBackgroundTaskManager assertion:didInvalidateWithError:]):
(-[WKProcessAssertionBackgroundTaskManager _handleBackgroundTaskExpiration]):
(-[WKProcessAssertionBackgroundTaskManager _releaseBackgroundTask]):

Source/WTF:

Add build-time flag for WebKit-specific assertion in RunningBoard.

* wtf/PlatformHave.h:


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@259414 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WTF/ChangeLog b/Source/WTF/ChangeLog
index 25e477f..04b152b 100644
--- a/Source/WTF/ChangeLog
+++ b/Source/WTF/ChangeLog
@@ -1,3 +1,15 @@
+2020-04-02  Chris Dumez  <cdumez@apple.com>
+
+        [iOS] Replace UIKit background task with a RunningBoard FinishTaskInterruptable assertion
+        https://bugs.webkit.org/show_bug.cgi?id=209825
+        <rdar://problem/61118503>
+
+        Reviewed by Geoffrey Garen.
+
+        Add build-time flag for WebKit-specific assertion in RunningBoard.
+
+        * wtf/PlatformHave.h:
+
 2020-04-02  Keith Rollin  <krollin@apple.com>
 
         Address static analysis warning in DataLog.cpp: Value stored to 'pathCharactersAvailable' is never read
diff --git a/Source/WTF/wtf/PlatformHave.h b/Source/WTF/wtf/PlatformHave.h
index 37b2f16..36a3f46 100644
--- a/Source/WTF/wtf/PlatformHave.h
+++ b/Source/WTF/wtf/PlatformHave.h
@@ -456,6 +456,12 @@
 #define HAVE_CFNETWORK_ALTERNATIVE_SERVICE 1
 #endif
 
+#if (PLATFORM(IOS_FAMILY) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 140000) \
+    || (PLATFORM(WATCHOS) && __WATCH_OS_VERSION_MIN_REQUIRED >= 70000) \
+    || (PLATFORM(APPLETV) && __TV_OS_VERSION_MIN_REQUIRED >= 140000)
+#define HAVE_RUNNINGBOARD_WEBKIT_ASSERTIONS 1
+#endif
+
 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500
 #define HAVE_CSCHECKFIXDISABLE 1
 #endif
diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog
index 85798c4..5399ee2 100644
--- a/Source/WebKit/ChangeLog
+++ b/Source/WebKit/ChangeLog
@@ -1,3 +1,28 @@
+2020-04-02  Chris Dumez  <cdumez@apple.com>
+
+        [iOS] Replace UIKit background task with a RunningBoard FinishTaskInterruptable assertion
+        https://bugs.webkit.org/show_bug.cgi?id=209825
+        <rdar://problem/61118503>
+
+        Reviewed by Geoffrey Garen.
+
+        Replace UIKit background task with a RunningBoard FinishTaskInterruptable assertion on iOS.
+        Our UIProcess gets terminated too frequently when the UIKit background task expires when
+        the UIProcess holds it for longer than 30 seconds in the background. The RunningBoard
+        FinishTaskInterruptable assertion is supposed to be equivalent but would cause suspension
+        of our UIProcess on expiration, instead of termination.
+
+        * UIProcess/ios/ProcessAssertionIOS.mm:
+        (-[WKProcessAssertionBackgroundTaskManager init]):
+        (-[WKProcessAssertionBackgroundTaskManager _scheduleReleaseTask]):
+        (-[WKProcessAssertionBackgroundTaskManager _cancelPendingReleaseTask]):
+        (-[WKProcessAssertionBackgroundTaskManager _hasBackgroundTask]):
+        (-[WKProcessAssertionBackgroundTaskManager _updateBackgroundTask]):
+        (-[WKProcessAssertionBackgroundTaskManager assertionWillInvalidate:]):
+        (-[WKProcessAssertionBackgroundTaskManager assertion:didInvalidateWithError:]):
+        (-[WKProcessAssertionBackgroundTaskManager _handleBackgroundTaskExpiration]):
+        (-[WKProcessAssertionBackgroundTaskManager _releaseBackgroundTask]):
+
 2020-04-02  Per Arne Vollan  <pvollan@apple.com>
 
         [iOS] Allow use of syscall from the WebContent sandbox
diff --git a/Source/WebKit/Platform/spi/ios/RunningBoardServicesSPI.h b/Source/WebKit/Platform/spi/ios/RunningBoardServicesSPI.h
index 97e0cb1..05a35da 100644
--- a/Source/WebKit/Platform/spi/ios/RunningBoardServicesSPI.h
+++ b/Source/WebKit/Platform/spi/ios/RunningBoardServicesSPI.h
@@ -48,4 +48,9 @@
 - (void)invalidate;
 @end
 
+@protocol RBSAssertionObserving <NSObject>
+- (void)assertionWillInvalidate:(RBSAssertion *)assertion;
+- (void)assertion:(RBSAssertion *)assertion didInvalidateWithError:(NSError *)error;
+@end
+
 #endif
diff --git a/Source/WebKit/UIProcess/ios/ProcessAssertionIOS.mm b/Source/WebKit/UIProcess/ios/ProcessAssertionIOS.mm
index 31c1097..1b63ecc 100644
--- a/Source/WebKit/UIProcess/ios/ProcessAssertionIOS.mm
+++ b/Source/WebKit/UIProcess/ios/ProcessAssertionIOS.mm
@@ -30,6 +30,7 @@
 
 #import "AssertionServicesSPI.h"
 #import "Logging.h"
+#import "RunningBoardServicesSPI.h"
 #import "WebProcessPool.h"
 #import <UIKit/UIApplication.h>
 #import <wtf/HashMap.h>
@@ -44,7 +45,12 @@
 // on the expiration handler getting called).
 static const Seconds releaseBackgroundTaskAfterExpirationDelay { 2_s };
 
-@interface WKProcessAssertionBackgroundTaskManager : NSObject
+@interface WKProcessAssertionBackgroundTaskManager
+#if HAVE(RUNNINGBOARD_WEBKIT_ASSERTIONS)
+    : NSObject <RBSAssertionObserving>
+#else
+    : NSObject
+#endif
 
 + (WKProcessAssertionBackgroundTaskManager *)shared;
 
@@ -55,7 +61,12 @@
 
 @implementation WKProcessAssertionBackgroundTaskManager
 {
+#if HAVE(RUNNINGBOARD_WEBKIT_ASSERTIONS)
+    RetainPtr<RBSAssertion> _backgroundTask;
+#else
     UIBackgroundTaskIdentifier _backgroundTask;
+#endif
+
     WeakHashSet<ProcessAndUIAssertion> _assertionsNeedingBackgroundTask;
     BOOL _applicationIsBackgrounded;
     dispatch_block_t _pendingTaskReleaseTask;
@@ -73,7 +84,9 @@
     if (!self)
         return nil;
 
+#if !HAVE(RUNNINGBOARD_WEBKIT_ASSERTIONS)
     _backgroundTask = UIBackgroundTaskInvalid;
+#endif
 
     [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification object:[UIApplication sharedApplication] queue:nil usingBlock:^(NSNotification *) {
         _applicationIsBackgrounded = NO;
@@ -84,7 +97,7 @@
     [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:[UIApplication sharedApplication] queue:nil usingBlock:^(NSNotification *) {
         _applicationIsBackgrounded = YES;
         
-        if (_backgroundTask == UIBackgroundTaskInvalid)
+        if (![self _hasBackgroundTask])
             WebKit::WebProcessPool::notifyProcessPoolsApplicationIsAboutToSuspend();
     }];
 
@@ -132,7 +145,7 @@
     if (_pendingTaskReleaseTask)
         return;
 
-    RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager - _scheduleReleaseTask because the expiration handler has been called", self);
+    RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: _scheduleReleaseTask because the expiration handler has been called", self);
     _pendingTaskReleaseTask = dispatch_block_create((dispatch_block_flags_t)0, ^{
         _pendingTaskReleaseTask = nil;
         [self _releaseBackgroundTask];
@@ -149,57 +162,105 @@
     if (!_pendingTaskReleaseTask)
         return;
 
-    RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager - _cancelPendingReleaseTask because the application is foreground again", self);
+    RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: _cancelPendingReleaseTask because the application is foreground again", self);
     dispatch_block_cancel(_pendingTaskReleaseTask);
     _pendingTaskReleaseTask = nil;
 }
 
+- (BOOL)_hasBackgroundTask
+{
+#if HAVE(RUNNINGBOARD_WEBKIT_ASSERTIONS)
+    return !!_backgroundTask;
+#else
+    return _backgroundTask != UIBackgroundTaskInvalid;
+#endif
+}
+
 - (void)_updateBackgroundTask
 {
-    if (!_assertionsNeedingBackgroundTask.computesEmpty() && _backgroundTask == UIBackgroundTaskInvalid) {
+    if (!_assertionsNeedingBackgroundTask.computesEmpty() && ![self _hasBackgroundTask]) {
         if (_applicationIsBackgrounded) {
             RELEASE_LOG_ERROR(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: Ignored request to start a new background task because the application is already in the background", self);
             return;
         }
-        RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager - beginBackgroundTaskWithName", self);
+        RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: beginBackgroundTaskWithName", self);
+#if HAVE(RUNNINGBOARD_WEBKIT_ASSERTIONS)
+        RBSTarget *target = [RBSTarget currentProcess];
+        RBSDomainAttribute *domainAttribute = [RBSDomainAttribute attributeWithDomain:@"com.apple.common" name:@"FinishTaskInterruptable"];
+        _backgroundTask = adoptNS([[RBSAssertion alloc] initWithExplanation:@"WebKit UIProcess background task" target:target attributes:@[domainAttribute]]);
+        [_backgroundTask addObserver:self];
+
+        NSError *acquisitionError = nil;
+        if (![_backgroundTask acquireWithError:&acquisitionError])
+            RELEASE_LOG_ERROR(ProcessSuspension, "WKProcessAssertionBackgroundTaskManager: Failed to acquire FinishTaskInterruptable assertion for own process, error: %{public}@", acquisitionError);
+        else
+            RELEASE_LOG(ProcessSuspension, "WKProcessAssertionBackgroundTaskManager: Successfully took a FinishTaskInterruptable assertion for own process");
+#else
         _backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"com.apple.WebKit.ProcessAssertion" expirationHandler:^{
-            RELEASE_LOG_ERROR(ProcessSuspension, "Background task expired while holding WebKit ProcessAssertion (isMainThread? %d).", RunLoop::isMain());
-            if (!_applicationIsBackgrounded) {
-                // We've received the invalidation warning after the app has become foreground again. In this case, we should not
-                // warn clients of imminent suspension. To be safe (avoid potential killing), we end the task right away and call
-                // _updateBackgroundTask asynchronously to start a new task if necessary.
-                [self _releaseBackgroundTask];
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    [self _updateBackgroundTask];
-                });
-                return;
-            }
-
-            // The expiration handler gets called on a non-main thread when the underlying assertion could not be taken (rdar://problem/27278419).
-            if (RunLoop::isMain())
-                [self _notifyAssertionsOfImminentSuspension];
-            else {
-                dispatch_sync(dispatch_get_main_queue(), ^{
-                    [self _notifyAssertionsOfImminentSuspension];
-                });
-            }
-
-            [self _scheduleReleaseTask];
+            [self _handleBackgroundTaskExpiration];
         }];
+#endif
     } else if (_assertionsNeedingBackgroundTask.computesEmpty())
         [self _releaseBackgroundTask];
 }
 
+#if HAVE(RUNNINGBOARD_WEBKIT_ASSERTIONS)
+- (void)assertionWillInvalidate:(RBSAssertion *)assertion
+{
+    ASSERT(assertion == _backgroundTask.get());
+    [self _handleBackgroundTaskExpiration];
+}
+
+- (void)assertion:(RBSAssertion *)assertion didInvalidateWithError:(NSError *)error
+{
+    ASSERT(assertion == _backgroundTask.get());
+    RELEASE_LOG_ERROR(ProcessSuspension, "WKProcessAssertionBackgroundTaskManager: FinishTaskInterruptable assertion was invalidated, error: %{public}@", error);
+}
+#endif
+
+- (void)_handleBackgroundTaskExpiration
+{
+    RELEASE_LOG(ProcessSuspension, "WKProcessAssertionBackgroundTaskManager: Background task expired while holding WebKit ProcessAssertion (isMainThread? %d).", RunLoop::isMain());
+    if (!_applicationIsBackgrounded) {
+        // We've received the invalidation warning after the app has become foreground again. In this case, we should not
+        // warn clients of imminent suspension. To be safe (avoid potential killing), we end the task right away and call
+        // _updateBackgroundTask asynchronously to start a new task if necessary.
+        [self _releaseBackgroundTask];
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [self _updateBackgroundTask];
+        });
+        return;
+    }
+
+    // The expiration handler gets called on a non-main thread when the underlying assertion could not be taken (rdar://problem/27278419).
+    if (RunLoop::isMain())
+        [self _notifyAssertionsOfImminentSuspension];
+    else {
+        dispatch_sync(dispatch_get_main_queue(), ^{
+            [self _notifyAssertionsOfImminentSuspension];
+        });
+    }
+
+    [self _scheduleReleaseTask];
+}
+
 - (void)_releaseBackgroundTask
 {
-    if (_backgroundTask == UIBackgroundTaskInvalid)
+    if (![self _hasBackgroundTask])
         return;
 
-    RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager - endBackgroundTask", self);
+    RELEASE_LOG(ProcessSuspension, "%p - WKProcessAssertionBackgroundTaskManager: endBackgroundTask", self);
     if (_applicationIsBackgrounded)
         WebKit::WebProcessPool::notifyProcessPoolsApplicationIsAboutToSuspend();
+
+#if HAVE(RUNNINGBOARD_WEBKIT_ASSERTIONS)
+    [_backgroundTask removeObserver:self];
+    [_backgroundTask invalidate];
+    _backgroundTask = nullptr;
+#else
     [[UIApplication sharedApplication] endBackgroundTask:_backgroundTask];
     _backgroundTask = UIBackgroundTaskInvalid;
+#endif
 }
 
 @end